Node Selectors: target a Pod at the right node
How labels on nodes and spec.nodeSelector split the cluster by purpose, and why a typo sends the Pod to Pending.
How to send a Pod only to nodes with the properties you need, what happens when no such node exists, and where to look for the cause.
Why this matters
Nodes in a cluster are rarely identical. Some have GPUs for ML, some have
fast SSDs for databases, some are cheap spot instances for batch jobs. You
need a way to tell the scheduler: "only place this Pod on nodes like
these." The simplest way is nodeSelector.
What nodeSelector is
nodeSelector is the simplest Pod placement mechanism. You write a map of
labels in the spec, and the Pod can only land on nodes that have those
labels set. It works like a label selector in a Service, but it targets
nodes.
You set labels on nodes, write a nodeSelector in each Pod, and the
scheduler picks a matching node on its own.
Unlike nodeName, which pins a Pod to a node by name, nodeSelector is
declarative: "I want any node with these properties." If three nodes match,
the scheduler picks any one of them, factoring in resources. If none match,
the Pod sits in Pending and waits.
Minimal YAML
apiVersion: v1
kind: Pod
metadata:
name: db
spec:
nodeSelector:
disk: ssd
tier: production
containers:
- name: db
image: postgres:16.4-alpine
All keys in nodeSelector combine with AND. The node needs both
disk=ssd and tier=production at the same time. If the node has only
disk=ssd, the Pod stays Pending.
In a Deployment, nodeSelector goes into the Pod template, that is
spec.template.spec.nodeSelector. The field spec.nodeSelector at the
Deployment's own level doesn't exist.
Labels on nodes
Labels are set with kubectl label nodes:
kubectl label nodes node-1 disk=ssd
kubectl label nodes node-1 tier=production
kubectl get nodes --show-labels
kubectl get nodes -L disk -L tier
-L prints the label's value as its own column, which makes nodes easier
to compare:
NAME STATUS ROLES AGE VERSION DISK TIER
node-1 Ready control-plane 6m v1.35.0 ssd production
Every node has built-in labels, set by kubelet: kubernetes.io/hostname,
kubernetes.io/os, kubernetes.io/arch. You can select nodes by these too,
which is handy on clusters with both amd64 and arm64 nodes. In the cloud,
you also get node.kubernetes.io/instance-type and
topology.kubernetes.io/zone, but the cloud provider fills those in. A
local cluster doesn't have them.
How it works
nodeSelectoronly does strict equality on key and value. For "OR", "NOT", or "one of a list" you neednodeAffinity.- The selector is checked only at placement. Remove a label from a node, and a Pod that's already running stays put. Add a label, and new Pods land where they should, but old ones don't move.
- The scheduler doesn't fail if no node matches. The Pod just sits in
Pending. Once you add the right label to a node, the scheduler places the Pod on its next attempt, nothing needs to be recreated. - A label's value is at most 63 characters. A key is an optional prefix (up
to 253 characters, for example
node.kubernetes.io/) plus a name of up to 63 characters.
Even on a single-node cluster you can see the whole mechanism. With the
right label the Pod starts, and the moment you put a value in the selector
that the node doesn't have, the Pod goes to Pending.
The cause shows up in kubectl describe pod, in the Events block:
Warning FailedScheduling 12s default-scheduler 0/1 nodes are available: 1 node(s) didn't match Pod's node affinity/selector. ...
That means the node is missing that label, or there's a typo in the key or the value.
nodeSelector, nodeName, and nodeAffinity
nodeName. One node by name. The scheduler isn't involved.nodeSelector. Match by labels, all keys combined with AND, strict equality.nodeAffinity. The same, plus OR, operatorsIn,NotIn,Exists,Gt,Lt, and soft rules with weights (preferred) instead of mandatory ones.
In production, teams use either nodeSelector, when plain equality is
enough, or nodeAffinity, when they need preferences or more complex
logic.
Related commands
kubectl label nodes <name> disk=ssd
kubectl label nodes <name> disk=nvme --overwrite # change the value
kubectl label nodes <name> disk- # remove the label
kubectl get nodes --show-labels
kubectl get nodes -l disk=ssd
kubectl get nodes -L disk
kubectl describe pod <name> # Events block, if Pending
kubectl explain pod.spec.nodeSelector
Without --overwrite, kubectl refuses to change a value that's already
set.
Common mistakes
disk: SSD(uppercase) in the selector, while the node hasdisk=ssd. Equality is strict, so this won't match.- A value without quotes:
gpu: true. YAML reads it as a boolean, butnodeSelectoronly accepts strings, and the API rejects the manifest with the errorcannot unmarshal bool into Go struct field PodSpec.spec.nodeSelector of type string. Writegpu: "true". - The label also exists on a different group of nodes. The Pod lands in the wrong place. Labels need to be precise and meaningful.
- The node matches on labels, but the Pod is still
Pending, withhad untolerated taint(s)in the events.nodeSelectordoesn't get past a taint, you need tolerations for that (lesson on taints and tolerations).
What you'll need for the tasks
kubectl get nodes --show-labelsandkubectl get nodes -L <key>: what labels are actually set on a node.kubectl get deployment <name> -o yaml: wherenodeSelectorlives in a Deployment and what it says.kubectl describe pod <name>: theFailedSchedulingevent and its text.kubectl label nodes <node> key=value, and removing a label withkey-.- You don't need to recreate a Pod: after you fix a Deployment or a node's label, the scheduler places the Pod on its own.
The tasks live in the labs/ folder: each lab brings up its own kind
cluster on your machine, instructions in its README.md.
Homework
Practice the theory on your own cluster: 1 step with a check. Set up the cluster with the Kubernetes locally track, the tasks live in the k8s-practice repository.
./run.sh start k8s-node-selectors # set up the starting state
./run.sh verify k8s-node-selectors # check the current step
In short
nodeSelectoris a map of labels in a Pod's spec, all keys combined with AND, strict equality only.- Labels are set on nodes with
kubectl label nodes; built-in ones (kubernetes.io/archand others) exist on every node already. - If no node matches, the Pod stays
Pending, and the cause shows up in thekubectl describe podevents. - A label is checked only at placement: it doesn't move or evict Pods that are already running.
nodeSelectordoesn't get past taints. For OR logic and preferences you neednodeAffinity.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson k8s-scheduling-03-node-selectors
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress