How to Set Up Kubernetes Locally with Minikube: A Beginner’s Step-by-Step Tutorial

If you want to learn Kubernetes without renting a managed cluster and watching the invoice grow, Minikube is the shortest path. It spins up a single-node Kubernetes cluster on your own laptop in a couple of minutes, using Docker, Hyper-V, or a hypervisor as the backend.

This Kubernetes Minikube tutorial is deliberately practical. You will not just read theory: you will build a tiny Node.js app, containerize it, deploy it to your local cluster, expose it with a Service, scale it to multiple pods, roll out a new version, and then clean everything up. Along the way we flag the failures that break most beginners on day one: driver detection problems, ImagePullBackOff, and port forwarding that silently does nothing.

What You Will Build in This Tutorial

  • A working single-node Kubernetes cluster running on your machine
  • A containerized Node.js web app, built locally and loaded straight into the cluster
  • A Deployment that keeps your pods alive and a Service that exposes them
  • Horizontal scaling from 1 pod to 4 pods with a single command
  • A zero-downtime rolling update, plus a rollback
  • A troubleshooting playbook you can reuse on every future cluster

Estimated time: 30 to 40 minutes. Difficulty: beginner. Cost: zero.

kubernetes laptop containers

Minikube, kubectl and kubeadm: Who Does What?

Beginners mix these three up constantly, so let us clear it up before typing any command.

Tool What it is When you use it
Minikube A tool that creates and manages a local Kubernetes cluster inside a container or VM Learning, local development, CI sandboxes
kubectl The official CLI that talks to the Kubernetes API of any cluster Every day, on local and production clusters alike
kubeadm A bootstrapper for building real multi-node clusters from scratch Production-style installs on your own servers

Short version: Minikube creates the cluster, kubectl drives it. Minikube even ships its own bundled kubectl (minikube kubectl -- get pods) if you would rather not install it separately.

Prerequisites

Requirement Minimum Recommended
CPU cores 2 4
RAM 2 GB free 4 to 8 GB free
Disk 20 GB free 40 GB free
Container runtime Docker, Podman, or a hypervisor Docker Desktop or Docker Engine
Internet Required for the first start Required

Tip: on Windows, use WSL 2 with Docker Desktop. Running Minikube from inside a WSL 2 Ubuntu shell gives the smoothest experience by far.

Step 1: Install the Driver (Docker Is the Easiest)

Minikube needs a driver: the thing that actually hosts the Kubernetes node. The docker driver is the default on most systems and the least painful.

  • macOS / Windows: install Docker Desktop and make sure the whale icon says “Docker Desktop is running”.
  • Linux: install Docker Engine, then add yourself to the docker group so you do not need sudo:
sudo usermod -aG docker $USER && newgrp docker
docker run --rm hello-world

If docker run hello-world works, your driver is ready. Alternative drivers include podman, hyperv, virtualbox, kvm2, and qemu.

kubernetes laptop containers

Step 2: Install Minikube

Linux (x86_64)

curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
rm minikube-linux-amd64

macOS (Intel and Apple Silicon)

brew install minikube

Windows (PowerShell as Administrator)

winget install Kubernetes.minikube

Verify:

minikube version

Step 3: Install kubectl

OS Command
macOS brew install kubectl
Windows winget install Kubernetes.kubectl
Linux (Debian/Ubuntu) sudo snap install kubectl --classic

Check it:

kubectl version --client

A useful habit: keep your kubectl within one minor version of your cluster. Minikube will warn you if the gap gets too wide.

Step 4: Start Your Cluster

This is the command that creates everything:

minikube start --driver=docker --cpus=2 --memory=4096

The first run pulls the base image and the Kubernetes components, so expect 2 to 5 minutes. Subsequent starts take seconds.

Useful start flags

Flag Purpose
--driver=docker Forces the Docker driver instead of auto-detection
--cpus=4 --memory=8192 Gives the node more resources for heavier workloads
--kubernetes-version=v1.34.0 Pins a specific Kubernetes version to match a target environment
--nodes=3 Creates a multi-node cluster to practice scheduling and affinity
-p dev Creates a named profile so you can run several clusters side by side

Verify the cluster is healthy

minikube status
kubectl get nodes
kubectl get pods -A

You should see one node in Ready state and a handful of system pods in Running state in the kube-system namespace. If the node says NotReady for more than a minute, jump to the troubleshooting section below.

kubernetes laptop containers

Step 5: Create a Tiny Node.js App

Create a folder called hello-node with three files.

server.js

const http = require('http');
const os = require('os');

const PORT = process.env.PORT || 3000;
const VERSION = process.env.APP_VERSION || 'v1';

const server = http.createServer((req, res) => {
  if (req.url === '/healthz') {
    res.writeHead(200);
    return res.end('ok');
  }
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello from pod ' + os.hostname() + ' (' + VERSION + ')\n');
});

server.listen(PORT, '0.0.0.0', () => {
  console.log('Listening on port ' + PORT);
});

Important detail: the server listens on 0.0.0.0, not 127.0.0.1. A container that binds only to localhost is unreachable from the Service, and this single line causes a huge share of “my app works in Docker but not in Kubernetes” tickets.

package.json

{
  "name": "hello-node",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  }
}

Dockerfile

FROM node:24-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
COPY . .
EXPOSE 3000
USER node
CMD ["node", "server.js"]

Step 6: Get the Image Into the Cluster (The Step Everyone Gets Wrong)

Minikube runs its own container runtime inside the node. An image you built on your host Docker daemon does not automatically exist in there. You have two clean options.

Option A: build directly inside Minikube’s Docker daemon

Linux / macOS:

eval $(minikube docker-env)
docker build -t hello-node:v1 .

Windows PowerShell:

& minikube -p minikube docker-env --shell powershell | Invoke-Expression
docker build -t hello-node:v1 .

To return to your normal Docker daemon later: eval $(minikube docker-env -u).

Option B: build on the host, then load the image

docker build -t hello-node:v1 .
minikube image load hello-node:v1

Confirm the image is really there:

minikube image ls | grep hello-node

Step 7: Write the Deployment

Create deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-node
  labels:
    app: hello-node
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hello-node
  template:
    metadata:
      labels:
        app: hello-node
    spec:
      containers:
        - name: hello-node
          image: hello-node:v1
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 3000
          env:
            - name: APP_VERSION
              value: "v1"
          resources:
            requests:
              cpu: "50m"
              memory: "64Mi"
            limits:
              cpu: "250m"
              memory: "128Mi"
          readinessProbe:
            httpGet:
              path: /healthz
              port: 3000
            initialDelaySeconds: 2
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /healthz
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 10

Do not skip imagePullPolicy. If your tag is latest, Kubernetes defaults to Always and will try to pull from Docker Hub, fail, and give you ErrImagePull. Use a real tag such as v1 plus IfNotPresent (or Never) for local images.

Apply it:

kubectl apply -f deployment.yaml
kubectl get pods -w

Wait until the pod shows 1/1 Running, then press Ctrl+C.

Step 8: Expose the App With a Service

Pods get ephemeral IPs. A Service gives you a stable address and load balances across pods. Create service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: hello-node-svc
spec:
  type: NodePort
  selector:
    app: hello-node
  ports:
    - port: 80
      targetPort: 3000
      nodePort: 30080
kubectl apply -f service.yaml
kubectl get svc hello-node-svc

Three ways to reach it

  1. The Minikube shortcut (opens a browser and prints the URL):
    minikube service hello-node-svc
  2. Get the URL only, for curl or scripts:
    curl $(minikube service hello-node-svc --url)
  3. Port forward, which works regardless of driver and firewall quirks:
    kubectl port-forward svc/hello-node-svc 8080:80

    Then open http://localhost:8080 in another terminal or browser.

You should see something like Hello from pod hello-node-7c9f8d6b4-2xq9p (v1).

Why the raw node IP sometimes does not work

With the docker driver on macOS and Windows, the Minikube node lives inside a Docker VM, so http://<minikube-ip>:30080 is often not routable from your host. That is normal and not a bug. Use minikube service or kubectl port-forward, or run minikube tunnel in a separate terminal when you need a LoadBalancer type Service.

kubernetes laptop containers

Step 9: Scale Your Pods

This is where Kubernetes starts to feel useful. Scale to four replicas:

kubectl scale deployment hello-node --replicas=4
kubectl get pods -o wide

Now hit the Service repeatedly and watch the hostname change as traffic is balanced:

URL=$(minikube service hello-node-svc --url)
for i in $(seq 1 10); do curl -s $URL; done

Kill a pod and watch the Deployment recreate it within seconds:

kubectl delete pod $(kubectl get pods -l app=hello-node -o jsonpath='{.items[0].metadata.name}')
kubectl get pods -w

Bonus: autoscaling with the metrics-server addon

minikube addons enable metrics-server
kubectl autoscale deployment hello-node --cpu-percent=50 --min=1 --max=5
kubectl get hpa

Metrics can take a minute or two to appear. Until then the HPA target column shows <unknown>, which is expected.

Step 10: Perform a Rolling Update and a Rollback

Change the message in server.js, then rebuild with a new tag:

docker build -t hello-node:v2 .
minikube image load hello-node:v2
kubectl set image deployment/hello-node hello-node=hello-node:v2
kubectl rollout status deployment/hello-node

Broke something? Roll back instantly:

kubectl rollout undo deployment/hello-node
kubectl rollout history deployment/hello-node

Always use a new tag for a new image. If you reuse the same tag, Kubernetes sees no change in the pod spec and will not restart anything.

Step 11: Explore With the Dashboard and Addons

minikube dashboard

Other addons worth enabling while learning:

  • minikube addons enable ingress for NGINX ingress and host-based routing
  • minikube addons enable storage-provisioner for dynamic PersistentVolumes (on by default)
  • minikube addons list to see everything available
kubernetes laptop containers

Step 12: Clean Up

kubectl delete -f service.yaml -f deployment.yaml
minikube stop          # keeps the cluster for next time
minikube delete        # removes the cluster entirely
minikube delete --all  # removes every profile

minikube stop is what you want at the end of a work day. minikube delete is the nuclear reset when a cluster is misbehaving.

Troubleshooting: The Errors That Actually Happen

1. Driver problems

Symptom: Exiting due to DRV_NOT_DETECTED or Unable to pick a default driver.

  • Make sure Docker is actually running: docker info must succeed.
  • On Linux, if you get a permission denied on the Docker socket, you forgot the docker group: sudo usermod -aG docker $USER then open a new shell.
  • Never run minikube start with the docker driver as root. You will get The "docker" driver should not be used with root privileges. Run it as your normal user.
  • On Windows, VirtualBox and Hyper-V conflict. Pick one. If Docker Desktop with WSL 2 is installed, just use --driver=docker.
  • Force a driver explicitly: minikube start --driver=docker or --driver=hyperv.

2. Image pull errors

Symptom: pod stuck in ErrImagePull or ImagePullBackOff.

Diagnose first:

kubectl describe pod <pod-name>
kubectl get events --sort-by=.metadata.creationTimestamp

Then apply the right fix:

  • The image only exists on your host Docker: run minikube image load hello-node:v1 or build inside minikube docker-env.
  • Tag is latest with default pull policy: switch to an explicit tag and imagePullPolicy: IfNotPresent.
  • Typo in the image name: check minikube image ls against your YAML, character by character.
  • Private registry: create a secret and reference it with imagePullSecrets.
  • Docker Hub rate limit: log in with docker login before building, or use a different base image mirror.

3. Port forwarding and “I cannot open the app”

  • kubectl port-forward runs in the foreground. If you close the terminal, the tunnel dies. Keep it open or add &.
  • bind: address already in use means something else owns that local port. Change it: kubectl port-forward svc/hello-node-svc 8081:80.
  • Port forward works but you get connection refused inside the pod: your app is listening on 127.0.0.1 instead of 0.0.0.0.
  • targetPort must match the port your container actually listens on, not the Service port.
  • Service returns nothing: check the selector matches the pod labels with kubectl get endpoints hello-node-svc. No endpoints means the selector is wrong or no pod is Ready.
  • LoadBalancer Service stuck in <pending>: run minikube tunnel in a separate terminal.

4. Pod stuck in Pending or CrashLoopBackOff

Status Most likely cause Fix
Pending Insufficient CPU or memory on the node Lower resource requests, or restart with --cpus=4 --memory=8192
CrashLoopBackOff App exits immediately or probe fails kubectl logs <pod> --previous to see the real error
CreateContainerConfigError Missing ConfigMap or Secret Create the referenced object first
OOMKilled Memory limit too low Raise resources.limits.memory

5. Minikube hangs on start

  • Run with verbose logs: minikube start --alsologtostderr -v=5
  • Behind a corporate proxy, export HTTP_PROXY, HTTPS_PROXY and crucially NO_PROXY=localhost,127.0.0.1,10.96.0.0/12,192.168.59.0/24
  • Low disk space on the Docker volume is a classic silent killer. Run docker system prune -a.
  • When all else fails: minikube delete --all --purge then start fresh.

Minikube and kubectl Command Cheat Sheet

Command What it does
minikube start Create or resume the cluster
minikube status Check host, kubelet and apiserver health
minikube ip Print the node IP address
minikube ssh Shell into the node itself
minikube image load IMAGE Push a local image into the cluster runtime
minikube service NAME --url Get a reachable URL for a Service
minikube tunnel Expose LoadBalancer Services on your host
kubectl get pods -o wide List pods with node and IP details
kubectl describe pod NAME The number one debugging command
kubectl logs -f NAME Stream container logs
kubectl exec -it NAME -- sh Open a shell inside a container
kubectl get endpoints SVC Check whether a Service actually matches pods

Where to Go Next

  1. Add a ConfigMap and a Secret, and mount them as environment variables
  2. Enable the ingress addon and route two apps through one hostname
  3. Attach a PersistentVolumeClaim and run a database pod
  4. Create a namespace per environment and practice context switching
  5. Package your manifests into a Helm chart or a Kustomize overlay
  6. Spin up a multi-node cluster with minikube start --nodes=3 and experiment with node affinity and taints

Everything you just did on your laptop translates directly to a managed cluster. The YAML is identical. Only the ingress, storage class and load balancer implementation change.

FAQ

What is Minikube and how does it work with Kubernetes?

Minikube is a tool that provisions a complete, single-node (or optionally multi-node) Kubernetes cluster on your local machine. It creates a container or virtual machine, installs the Kubernetes control plane and kubelet inside it, and writes the credentials into your ~/.kube/config so that kubectl can talk to it just like any remote cluster.

What is the difference between kubectl and Minikube?

Minikube manages the lifecycle of the local cluster itself: create, stop, delete, addons, image loading. kubectl is the universal client used to create and inspect Kubernetes objects such as Deployments, Services and Pods. You use Minikube once per session and kubectl constantly.

Should I use kubeadm or Minikube?

Use Minikube if your goal is to learn Kubernetes, develop applications, or run a quick test cluster on one machine. Use kubeadm if you need to build a real multi-machine cluster you will operate yourself, or if you are preparing for a certification that tests cluster bootstrapping.

Can I really learn Kubernetes in a couple of days?

You can learn the core objects (Pod, Deployment, Service, ConfigMap, Namespace) and be productive in a weekend using Minikube. Mastering networking, storage, RBAC, operators and production troubleshooting takes months of hands-on practice. Start with the tutorial above, then break things on purpose and fix them.

How much RAM does Minikube need?

The practical minimum is 2 CPUs and 2 GB of RAM allocated to the node. For anything beyond a demo app, allocate 4 CPUs and 4 to 8 GB with minikube start --cpus=4 --memory=8192.

Does Minikube work offline?

After the first successful start, yes, mostly. The base image and Kubernetes components are cached locally, so minikube start works without internet. Pulling new container images from a public registry obviously still requires a connection.

Why does my pod say ImagePullBackOff when the image exists on my machine?

Because the cluster uses its own container runtime, separate from your host Docker daemon. Either build inside Minikube with eval $(minikube docker-env) or run minikube image load your-image:tag, and set imagePullPolicy: IfNotPresent in your Deployment.

How do I reset a broken Minikube cluster?

Run minikube delete --all --purge, confirm Docker is healthy with docker info, free disk space with docker system prune -a, then start again with minikube start --driver=docker --alsologtostderr -v=3 so you can see exactly where it fails.