Multi-Node Clusters
Minikube's --nodes flag starts a cluster with multiple nodes — one control-plane and N-1 workers. This is invaluable for testing scheduling constraints, DaemonSets (which need multiple nodes to verify they run everywhere), and inter-node networking behaviour.
Starting a Multi-Node Cluster
# Start a 3-node cluster (1 control-plane + 2 workers)
minikube start --nodes=3 --driver=docker
# With a profile name and specific Kubernetes version
minikube start -p multinode-test --nodes=3 \
--driver=docker \
--kubernetes-version=v1.30.2 \
--cpus=2 --memory=2048
# Verify nodes are ready
kubectl get nodes -o wide
# NAME STATUS ROLES AGE VERSION INTERNAL-IP OS-IMAGE
# multinode-test Ready control-plane 90s v1.30.2 192.168.49.2 Ubuntu 22.04
# multinode-test-m02 Ready worker 60s v1.30.2 192.168.49.3 Ubuntu 22.04
# multinode-test-m03 Ready worker 45s v1.30.2 192.168.49.4 Ubuntu 22.04
Node Roles
By default Minikube names nodes as <profile> for the control-plane and <profile>-m02, <profile>-m03, etc. for workers. Kubernetes taints the control-plane node with node-role.kubernetes.io/control-plane:NoSchedule so regular pods schedule only on workers.
# Inspect node labels and taints
kubectl describe node multinode-test | grep -A 5 Taints
# Taints: node-role.kubernetes.io/control-plane:NoSchedule
kubectl describe node multinode-test-m02 | grep -A 5 Taints
# Taints: <none>
# Check node capacity
kubectl get nodes -o custom-columns=\
NAME:.metadata.name,CPU:.status.capacity.cpu,MEM:.status.capacity.memory
# NAME CPU MEM
# multinode-test 2 2048Mi
# multinode-test-m02 2 2048Mi
# multinode-test-m03 2 2048Mi
Adding Nodes to an Existing Cluster
# Add a node to a running cluster (without restart)
minikube node add -p multinode-test
# List nodes managed by Minikube
minikube node list -p multinode-test
# multinode-test 192.168.49.2
# multinode-test-m02 192.168.49.3
# multinode-test-m03 192.168.49.4
# multinode-test-m04 192.168.49.5
# Delete a specific worker node
minikube node delete multinode-test-m04 -p multinode-test
Node Labels & Taints
Adding Labels for Scheduling Tests
# Label nodes to simulate topology zones
kubectl label node multinode-test-m02 topology.kubernetes.io/zone=us-east-1a
kubectl label node multinode-test-m03 topology.kubernetes.io/zone=us-east-1b
# Add custom labels for workload placement
kubectl label node multinode-test-m02 workload=backend
kubectl label node multinode-test-m03 workload=frontend
# Verify labels
kubectl get nodes --show-labels | grep workload
Taints & Tolerations
# Taint a node to test toleration behaviour
kubectl taint node multinode-test-m03 dedicated=gpu:NoSchedule
# A pod without the toleration will not schedule on m03:
kubectl run test-notolerated --image=nginx
kubectl describe pod test-notolerated | grep -A 5 Events
# Events:
# Warning FailedScheduling ... 0/2 nodes are available: 1 node(s) had taint...
# A pod WITH the toleration schedules on m03:
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "gpu"
effect: "NoSchedule"
nodeSelector:
workload: frontend # still pin to m03
containers:
- name: gpu-app
image: nginx
EOF
# Remove the taint when done
kubectl taint node multinode-test-m03 dedicated=gpu:NoSchedule-
PodAffinity & Anti-Affinity Testing
# Deploy a backend pod and then an anti-affinity frontend
# The frontend should land on a DIFFERENT node from the backend
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 2
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- backend
topologyKey: kubernetes.io/hostname
containers:
- name: frontend
image: nginx
# Verify frontend replicas landed on different nodes from backend
kubectl get pods -o wide --selector=app=frontend
# NAME READY STATUS NODE
# frontend-xxxxx 1/1 Running multinode-test-m02
# frontend-yyyyy 1/1 Running multinode-test-m03
# Test DaemonSets — should run on all worker nodes
kubectl create -f - <<EOF
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-logger
spec:
selector:
matchLabels:
app: node-logger
template:
metadata:
labels:
app: node-logger
spec:
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
containers:
- name: logger
image: busybox
command: ["sh", "-c", "while true; do echo $(hostname); sleep 10; done"]
EOF
kubectl get pods -o wide -l app=node-logger
# Should have one pod per node (3 total)
Profiles
Each Minikube profile is a completely independent cluster with its own kubeconfig context, container/VM, addon set, and Kubernetes version. Switching profiles is instant — no restart needed.
Listing Profiles
# Show all profiles with status
minikube profile list
# |----------------|-----------|---------|--------------|------|---------|---------|-------|--------|
# | Profile | VM Driver | Runtime | IP | Port | Version | Status | Nodes | Active |
# |----------------|-----------|---------|--------------|------|---------|---------|-------|--------|
# | minikube | docker | docker | 192.168.49.2 | 8443 | v1.30.2 | Running | 1 | * |
# | multinode-test | docker | docker | 192.168.49.3 | 8443 | v1.30.2 | Running | 3 | |
# | legacy-k126 | docker | docker | 192.168.49.4 | 8443 | v1.26.0 | Stopped | 1 | |
# |----------------|-----------|---------|--------------|------|---------|---------|-------|--------|
# The asterisk (*) marks the currently active profile
Switching Profiles
# Switch active profile — also switches kubectl context
minikube profile multinode-test
kubectl config current-context
# multinode-test
# Or use kubectl directly to switch context
kubectl config use-context minikube
# Create a brand-new profile for a new project
minikube start -p my-new-project --driver=docker --kubernetes-version=v1.31.0
# Stop a specific profile (preserves data)
minikube stop -p legacy-k126
# Start a stopped profile
minikube start -p legacy-k126
# Delete a profile permanently (removes VM and all data)
minikube delete -p legacy-k126
# Delete ALL profiles
minikube delete --all --purge
alias mk='minikube -p $(kubectl config current-context)' to your shell config so Minikube commands automatically target whichever cluster kubectl is pointing at. This prevents accidentally running minikube stop on the wrong profile.
Networking in Multi-Node
NodePort Services
# Expose a deployment as a NodePort service
kubectl expose deployment myapp --type=NodePort --port=80
# Get the URL (Minikube handles IP + NodePort for you)
minikube service myapp --url -p multinode-test
# http://192.168.49.3:32000
# Open in browser automatically
minikube service myapp -p multinode-test
# If you need a specific node's IP
kubectl get nodes -o jsonpath='{.items[1].status.addresses[0].address}'
# 192.168.49.3
kubectl get svc myapp -o jsonpath='{.spec.ports[0].nodePort}'
# 32000
curl http://192.168.49.3:32000
LoadBalancer & Tunnel
# LoadBalancer services get <pending> EXTERNAL-IP by default
kubectl expose deployment myapp --type=LoadBalancer --port=80
kubectl get svc myapp
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# myapp LoadBalancer 10.96.0.100 <pending> 80:31234/TCP 5s
# minikube tunnel assigns a real IP from your localhost range
# Run in a SEPARATE terminal (requires sudo on Linux/macOS)
sudo minikube tunnel -p multinode-test
# Now EXTERNAL-IP is populated
kubectl get svc myapp
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# myapp LoadBalancer 10.96.0.100 127.0.0.1 80/TCP 30s
curl http://127.0.0.1/
minikube tunnel creates IP routes on your host. Press Ctrl+C to stop the tunnel, or run minikube tunnel --cleanup to explicitly remove leftover routes after unexpected termination.
Persistent Storage
Minikube ships with the storage-provisioner addon enabled by default. It provides a standard StorageClass backed by host paths inside the Minikube VM — enough for development PVCs.
# Check the default StorageClass
kubectl get storageclass
# NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE
# standard (default) k8s.io/minikube-hostpath Delete Immediate
# Create a PVC using the default StorageClass
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: myapp-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
# storageClassName: standard # optional — default is used if omitted
EOF
# Verify it binds (should go Bound immediately with dynamic provisioning)
kubectl get pvc myapp-data
# NAME STATUS VOLUME CAPACITY STORAGECLASS
# myapp-data Bound pvc-xxxxxxxx-xxxx-xxxx-xxxx-xxxx 1Gi standard
# Mount in a Pod
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: myapp-with-storage
spec:
containers:
- name: app
image: nginx
volumeMounts:
- mountPath: /data
name: app-storage
volumes:
- name: app-storage
persistentVolumeClaim:
claimName: myapp-data
EOF
# In multi-node clusters, hostPath PVs bind to a specific node.
# If your pod reschedules to a different node, it won't see the old data.
# For multi-node persistence tests, use the CSI hostpath driver addon:
minikube addons enable csi-hostpath-driver
minikube addons enable volumesnapshots
# This creates a csi-hostpath StorageClass
kubectl get storageclass
# NAME PROVISIONER RECLAIMPOLICY
# csi-hostpath-sc hostpath.csi.k8s.io Delete
# standard (default) k8s.io/minikube-hostpath Delete
Exercises
m02 as tier=backend and m03 as tier=frontend. Deploy two Deployments — one with nodeSelector: tier: backend and one with tier: frontend. Verify with kubectl get pods -o wide that each pod lands on the intended node.
busybox image that prints the node hostname every 10 seconds. Add a toleration for the control-plane taint. Verify you get 3 pods (one per node). Then add a 4th node with minikube node add and watch the DaemonSet automatically schedule a 4th pod.
LoadBalancer service. Observe the <pending> EXTERNAL-IP. Run minikube tunnel in a separate terminal (with sudo). Verify the EXTERNAL-IP is assigned and curl returns the nginx welcome page. Stop the tunnel and verify the EXTERNAL-IP returns to pending.
Key Takeaways
- Use
minikube start --nodes=Nto create a multi-node cluster for scheduling, affinity, and DaemonSet testing - Add nodes dynamically to a running cluster with
minikube node add— no restart required - Label nodes to simulate topology zones and test
nodeSelector,podAffinity, and anti-affinity rules - Profiles keep clusters completely isolated: different Kubernetes versions, different addon sets, different kubeconfig contexts
minikube serviceopens NodePort services in the browser;minikube tunnelprovides real IPs for LoadBalancer services- Default
standardStorageClass uses hostPath provisioning — adequate for dev; usecsi-hostpath-driverfor multi-node PVC tests