Minikube runs a local Kubernetes cluster in a VM or a Docker container. Its distinguishing feature is the add-on system: Ingress, metrics-server and a dashboard are one command each, rather than a Helm install and a configuration file.
# macOS
brew install minikube
# Linux
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
# Windows
winget install Kubernetes.minikubeYou also need a driver. Docker is the usual choice and works everywhere.
minikube start --cpus=4 --memory=8192ð minikube v1.34.0 on Darwin 15.0
⨠Using the docker driver based on existing profile
ð Done! kubectl is now configured to use "minikube" cluster
Set --cpus and --memory explicitly. The defaults of 2 CPUs and 2GB are enough for a bare cluster and not enough once you add an ingress controller or anything else. Insufficient cpu on a fresh minikube is nearly always this.
kubectl get nodes
minikube statusOther options worth knowing at start time:
minikube start --kubernetes-version=v1.31.2 # match a target cluster
minikube start --driver=docker # pick the driver explicitly
minikube start --nodes=3 # multi-nodeA multi-node cluster is worth creating. Single-node hides every scheduling, affinity and anti-affinity problem you will eventually meet in a real cluster.
This is why people choose minikube.
minikube addons list
minikube addons enable ingress
minikube addons enable metrics-server| Add-on | What it gives you |
|---|---|
ingress | An NGINX ingress controller, ready to use |
metrics-server | Makes kubectl top and the HPA work |
dashboard | The web UI |
registry | A local image registry inside the cluster |
csi-hostpath-driver | Dynamic PersistentVolume provisioning |
metrics-server is the one to enable immediately, without it kubectl top fails and every HorizontalPodAutoscaler reports <unknown>.
minikube dashboardA local cluster has no cloud load balancer, so there are three ways in.
NodePort, with a helper to give you the URL:
minikube service payments-api --urlhttp://192.168.49.2:31847
LoadBalancer, which needs minikube tunnel running in a separate terminal:
minikube tunnelWithout it, LoadBalancer Services sit at <pending> forever. The tunnel creates a route to the cluster and assigns them addresses, and it usually asks for a password because it modifies network routes.
Ingress, once the add-on is enabled:
minikube ip # e.g. 192.168.49.2# Add to /etc/hosts
192.168.49.2 payments.localOr skip all of it for quick checks:
kubectl port-forward service/payments-api 8080:80Minikube's cluster cannot see your host's Docker images. Rather than pushing to a registry, point your shell at minikube's own Docker daemon:
eval $(minikube docker-env)
docker build -t myapp:dev .The image is now built inside the cluster's daemon and immediately available. Set imagePullPolicy: IfNotPresent, with Always, or an unpinned latest tag which defaults to Always, the kubelet tries the registry anyway and fails.
eval $(minikube docker-env -u) # switch back to your normal DockerThe alternative, if you prefer not to change your shell environment:
minikube image load myapp:devSeveral independent clusters, useful for keeping projects apart or testing across Kubernetes versions:
minikube start -p payments --cpus=4 --memory=8192
minikube start -p legacy --kubernetes-version=v1.28.0
minikube profile list
minikube profile payments # set the default
kubectl config use-context paymentsEach profile is a separate VM or container with its own resources. They are not free, so delete the ones you are not using.
minikube stop # keep the cluster, free the resources
minikube start # resume it
minikube delete # remove it entirely
minikube delete --all # every profilestop then start is much faster than recreating, and preserves everything you deployed.
When something is genuinely wrong, minikube delete && minikube start is usually quicker than debugging the local cluster itself.
minikube logs # cluster component logs
minikube ssh # shell onto the node| Task | Command |
|---|---|
| Start sized properly | minikube start --cpus=4 --memory=8192 |
| Specific version | minikube start --kubernetes-version=v1.31.2 |
| Multi-node | minikube start --nodes=3 |
| Enable an add-on | minikube addons enable ingress |
| Service URL | minikube service <name> --url |
| Make LoadBalancer work | minikube tunnel |
| Node IP | minikube ip |
| Build into the cluster | eval $(minikube docker-env) |
| Load a built image | minikube image load <image> |
| New profile | minikube start -p <name> |
| Stop / delete | minikube stop / minikube delete |
--cpus and --memory. The 2GB default causes scheduling failures as soon as you add anything.ingress and metrics-server in one command each.minikube tunnel is what makes LoadBalancer Services work. Without it they stay <pending>.eval $(minikube docker-env) builds straight into the cluster, with imagePullPolicy: IfNotPresent.minikube stop beats delete when you just want the resources back.<MCQExercise exercise={{ id: 'k8s-minikube-dockerenv', title: 'A locally built image that will not run', description: 'You run eval $(minikube docker-env), build myapp:dev, and deploy it. The Pod still reports ImagePullBackOff. What is the most likely cause?', options: [ { id: 'a', text: 'imagePullPolicy is Always, either set explicitly or defaulted from an unpinned tag, so the kubelet tries a registry instead of using the local image' }, { id: 'b', text: 'docker-env only works for images tagged latest' }, { id: 'c', text: 'The image must be pushed to a registry regardless of which daemon built it' }, { id: 'd', text: 'minikube tunnel must be running for local images to be visible' }, ], correctId: 'a', explanation: 'Building through docker-env puts the image in the cluster's own daemon, so it is genuinely present. imagePullPolicy: Always instructs the kubelet to fetch from a registry anyway, and the fetch fails because the image was never pushed. Set IfNotPresent, and note that a tag of latest or no tag at all defaults the policy to Always.', wrongExplanations: { b: 'Any tag works. In fact latest is the tag most likely to cause this, because it makes Always the default policy.', c: 'Avoiding a registry is the entire point of docker-env. Once the policy permits it, the local image is used directly.', d: 'minikube tunnel routes external traffic to LoadBalancer Services. It plays no part in how images are pulled.', }, }} />
<MCQExercise exercise={{ id: 'k8s-minikube-resources', title: 'Pods pending on a fresh cluster', description: 'Immediately after minikube start with default settings, enabling the ingress add-on leaves Pods Pending with "Insufficient cpu". What is wrong?', options: [ { id: 'a', text: 'The default allocation of 2 CPUs and 2GB is too small once cluster components and an ingress controller are running' }, { id: 'b', text: 'The ingress add-on is incompatible with the docker driver' }, { id: 'c', text: 'metrics-server must be enabled before any other add-on' }, { id: 'd', text: 'Ingress requires a multi-node cluster to schedule its controller' }, ], correctId: 'a', explanation: 'Scheduling works from the sum of resource requests, and minikube's defaults leave very little headroom once the control plane components have made their reservations. The ingress controller's requests then do not fit. Restarting with --cpus=4 --memory=8192 resolves it, and is worth doing on any cluster you intend to install things into.', wrongExplanations: { b: 'The ingress add-on works with the docker driver and is among the most commonly used combinations.', c: 'Add-ons are independent of one another. metrics-server affects kubectl top and autoscaling, not scheduling capacity.', d: 'The ingress controller runs happily on a single node. The constraint here is available CPU, not node count.', }, }} />
For a NodePort Service, minikube service <name> --url prints a working URL. For a LoadBalancer Service you need minikube tunnel running in a separate terminal, otherwise the external IP stays <pending> indefinitely. With the ingress add-on enabled, minikube ip gives the address to point a hosts-file entry at. For a quick check none of that is necessary, kubectl port-forward service/<name> 8080:80 works immediately.
There is no cloud provider to create a load balancer, so nothing ever assigns the external IP. minikube tunnel fills that role by creating a network route into the cluster and assigning addresses to LoadBalancer Services. It has to keep running in its own terminal, and it usually prompts for a password because it modifies system routes.
Run eval $(minikube docker-env) to point your shell at minikube's own Docker daemon, then build normally. The image lands directly in the cluster with no registry involved. Set imagePullPolicy: IfNotPresent on the container, since Always, which is also the default for an unpinned or latest tag, makes the kubelet attempt a registry pull that fails. eval $(minikube docker-env -u) switches your shell back. Alternatively, minikube image load myapp:dev copies an already-built image in.
stop shuts the cluster down while preserving its state, so minikube start brings everything back including your deployments. Much faster than recreating. delete removes the cluster and all its data permanently. Use stop to reclaim memory and CPU between work sessions, and delete when you want a clean slate or when troubleshooting the local cluster itself has become slower than rebuilding it.
Yes, using profiles: minikube start -p payments and minikube start -p legacy --kubernetes-version=v1.28.0 create independent clusters, each with its own kubectl context. minikube profile list shows them and minikube profile <name> sets the default for minikube commands. Each profile is a separate VM or container consuming real CPU and memory, so delete profiles you are not actively using.
Override Helm values with values.yaml files and --set, understand precedence, upgrade safely with --atomic, use helm rollback and fix a stuck release.
Monitor Kubernetes with metrics-server, kubectl top, Prometheus and kube-state-metrics. Learn what to watch, which alerts matter, and how logs fit in.
Learn what Kubernetes is and what problem it solves, how it differs from Docker and Docker Compose, the core objects to know, and whether you really need it.