When deploying applications in Kubernetes, the scheduler normally decides which node should run each Pod. In many environments, this automatic behavior is exactly what you want.
However, there are situations where you may need a workload to run on one particular node.
For example:
- a node may contain special hardware;
- a local storage volume may exist only on one server;
- a development environment may need to stay isolated;
- a database and application may need to run close together;
- a specific node may have more CPU, memory, or disk capacity;
- a temporary test deployment may need predictable placement.
A simple way to control Pod placement is to use Kubernetes nodeSelector.
This beginner-friendly guide explains what nodeSelector is, where it belongs in a Kubernetes YAML file, which resources support it, how to apply it to a Laravel application and MySQL database, and what risks should be considered before forcing workloads onto one node.
What Is Kubernetes nodeSelector?
nodeSelector is a Kubernetes scheduling rule.
It tells Kubernetes:
Run this Pod only on a node that has these labels.
Every Kubernetes node has labels. One common label is:
kubernetes.io/hostname
This label usually contains the Kubernetes node name.
For example:
kubernetes.io/hostname: worker-node-01.example.local
A Pod can be restricted to that node by adding:
nodeSelector:
kubernetes.io/hostname: worker-node-01.example.local
Kubernetes will then schedule the Pod only on the matching node.
Why Would You Pin a Workload to One Node?
By default, Kubernetes tries to place workloads where resources are available.
That is useful because Kubernetes can:
- distribute Pods;
- move workloads after failures;
- use available CPU and memory;
- avoid overloading one server;
- improve application resilience.
Still, manual placement can make sense in certain cases.
1. Local or Node-Specific Storage
A persistent volume may be physically attached to only one server.
If the Pod starts on another node, it may not be able to mount the volume.
2. Hardware Requirements
A workload may require hardware such as:
- a GPU;
- a large local SSD;
- a special network interface;
- high-memory hardware;
- a licensed device.
3. Development and Testing
In a development cluster, it may be useful to keep all components on one worker node for easier troubleshooting.
4. Data Locality
An application and its database may be intentionally placed on the same server to reduce network latency.
5. Temporary Migration or Maintenance
Administrators sometimes move a workload to one node while testing upgrades or repairing another server.
Which Kubernetes Resources Accept nodeSelector?
A common beginner mistake is to add nodeSelector to every Kubernetes resource.
That is not correct.
nodeSelector belongs inside a Pod specification.
It can therefore be used in resources that create Pods, including:
DeploymentStatefulSetDaemonSetJobCronJob- standalone
Pod
It does not belong directly in resources such as:
SecretConfigMapServicePersistentVolumeClaimIngressNamespace
These resources do not directly create running Pods.
For example, a Secret only stores configuration data. A Service exposes Pods over the network. An Ingress defines external routing. None of them are scheduled onto nodes.
Correct Location of nodeSelector
For a Deployment, nodeSelector must be added under:
spec:
template:
spec:
Example:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-application
spec:
replicas: 1
selector:
matchLabels:
app: web-application
template:
metadata:
labels:
app: web-application
spec:
nodeSelector:
kubernetes.io/hostname: worker-node-01.example.local
containers:
- name: application
image: example/application:latest
The important structure is:
spec:
template:
spec:
nodeSelector:
The same structure is used inside a StatefulSet.
Adding nodeSelector to a MySQL StatefulSet
Databases are commonly deployed as a StatefulSet.
A simplified MySQL example may look like this:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
namespace: example-development
spec:
serviceName: database
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
nodeSelector:
kubernetes.io/hostname: worker-node-01.example.local
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_DATABASE
value: application
- name: MYSQL_USER
value: application
- name: MYSQL_PASSWORD
value: change-me
ports:
- containerPort: 3306
volumeMounts:
- name: data
mountPath: /var/lib/mysql
This configuration tells Kubernetes to start the MySQL Pod only on the node named:
worker-node-01.example.local
The StatefulSet object itself is not placed on the node. The Pod created by the StatefulSet is.
Adding nodeSelector to an Application Deployment
A Laravel application may run in a Deployment with multiple containers.
For example:
- one PHP-FPM container;
- one Nginx container;
- one or more init containers.
All containers inside the same Pod always run on the same node.
Therefore, only one nodeSelector is needed for the Pod.
Example:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-application
namespace: example-development
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: web-application
template:
metadata:
labels:
app: web-application
spec:
nodeSelector:
kubernetes.io/hostname: worker-node-01.example.local
imagePullSecrets:
- name: registry-credentials
initContainers:
- name: wait-for-database
image: busybox:1.36
command:
- sh
- -c
- |
echo "Waiting for database..."
until nc -z database 3306;
do
echo "Database is not ready..."
sleep 2
done
echo "Database is ready."
containers:
- name: php
image: registry.example.local/application:latest
command:
- /bin/sh
- -c
args:
- exec php-fpm -F
- name: nginx
image: nginx:alpine
The init container, PHP container, and Nginx container all run inside the same Pod.
Because the Pod contains the nodeSelector, every container in that Pod runs on the selected node.
Why nodeSelector Is Not Added to Services
A Kubernetes Service does not run on a node in the same way as a Pod.
A Service is a networking abstraction.
It selects Pods using labels:
spec:
selector:
app: mysql
For example:
apiVersion: v1
kind: Service
metadata:
name: database
namespace: example-development
spec:
selector:
app: mysql
ports:
- port: 3306
clusterIP: None
This Service discovers the MySQL Pod because the Pod has the label:
app: mysql
The physical node where the Pod runs does not need to be declared in the Service.
Why nodeSelector Is Not Added to PersistentVolumeClaims
A PersistentVolumeClaim, often shortened to PVC, requests storage.
Example:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: application-storage
namespace: example-development
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
The PVC does not create a Pod and is not scheduled like a container.
Storage placement depends on:
- the storage class;
- the CSI driver;
- the underlying storage system;
- volume binding mode;
- node affinity on the resulting persistent volume;
- whether the storage is local or network-based.
Pinning a Pod to a node does not automatically guarantee that its volume can be mounted there.
That must be verified separately.
Important Storage Risk
The most important concern when forcing a database or application onto one node is storage compatibility.
Suppose a PVC is backed by local storage on another node.
The Pod may remain in a Pending state because:
- the volume belongs to a different node;
- the selected node cannot access the disk;
- the storage class created a volume with incompatible node affinity;
- the volume cannot be attached to the chosen server.
You may see errors such as:
0/4 nodes are available
or:
volume node affinity conflict
or:
pod has unbound immediate PersistentVolumeClaims
Before using nodeSelector, check the PVC and persistent volume details.
Useful commands include:
kubectl get pvc -n example-development
kubectl get pv
kubectl describe pvc application-storage -n example-development
kubectl describe pv <persistent-volume-name>
Look for fields such as:
nodeAffinity:
and:
storageClassName:
Verify the Node Name Before Deployment
The hostname in nodeSelector must match the node label exactly.
Check your cluster nodes:
kubectl get nodes
For more details:
kubectl get nodes --show-labels
You may see output similar to:
NAME STATUS ROLES AGE VERSION
worker-node-01.example.local Ready <none> 40d v1.34.0
worker-node-02.example.local Ready <none> 40d v1.34.0
control-plane.example.local Ready master 40d v1.34.0
You can inspect one node directly:
kubectl get node worker-node-01.example.local --show-labels
Verify that this label exists:
kubernetes.io/hostname=worker-node-01.example.local
If the value is misspelled, the Pod will not start.
Apply the Updated Kubernetes Manifest
After adding nodeSelector, apply the YAML file:
kubectl apply -f deployment.yaml
Then check the Pods:
kubectl get pods -n example-development -o wide
The NODE column shows where each Pod is running.
Example:
NAME READY STATUS NODE
mysql-0 1/1 Running worker-node-01.example.local
web-application-7dcf88b8d5-abc12 2/2 Running worker-node-01.example.local
This confirms that both workloads were scheduled on the intended node.
Restart Existing Workloads After the Change
Updating the Pod template normally causes Kubernetes to recreate the Pods.
For a Deployment, you can also restart it manually:
kubectl rollout restart deployment web-application \
-n example-development
Monitor the rollout:
kubectl rollout status deployment web-application \
-n example-development
For the StatefulSet:
kubectl rollout restart statefulset mysql \
-n example-development
Then verify:
kubectl get pods -n example-development -o wide
Troubleshooting a Pod Stuck in Pending
If the selected node is unavailable or unsuitable, the Pod may stay in Pending.
Start with:
kubectl get pods -n example-development
Then describe the Pod:
kubectl describe pod <pod-name> -n example-development
Check the Events section.
Common messages include:
Node Label Does Not Match
node(s) didn't match Pod's node affinity/selector
Possible causes:
- incorrect hostname;
- wrong capitalization;
- missing label;
- old node name;
- copied value does not exactly match the node label.
Insufficient CPU
Insufficient cpu
The selected node does not have enough available CPU for the Pod request.
Insufficient Memory
Insufficient memory
The selected node does not have enough memory.
Untolerated Taint
node(s) had untolerated taint
The node has a taint, and the Pod does not have a matching toleration.
Storage Conflict
volume node affinity conflict
The persistent volume cannot be used on the selected node.
The Availability Trade-Off
Pinning a workload to a single node reduces Kubernetes flexibility.
Without nodeSelector, Kubernetes may restart a Pod on another healthy node after a server failure.
With a strict hostname selector, Kubernetes can use only one node.
If that node becomes:
- offline;
- drained;
- disconnected;
- resource-constrained;
- under maintenance;
the Pod may remain unavailable.
Kubernetes will not move it to another node because no other node matches the selector.
This is especially important for:
- production databases;
- customer-facing web applications;
- workloads requiring high availability;
- critical background jobs.
When nodeSelector Is Appropriate
nodeSelector is a good choice when:
- the workload must run on a specific server;
- the node has required hardware;
- storage is tied to that node;
- the environment is non-production;
- the cluster is small;
- the placement rule is simple;
- predictable scheduling is more important than failover.
It may not be ideal when:
- high availability is required;
- the workload should survive node failures;
- several nodes can run the application;
- the cluster changes frequently;
- the hostname may be replaced;
- storage is portable across nodes;
- automatic load distribution is desired.
A Better Long-Term Approach: Custom Node Labels
Using the hostname works, but it tightly couples the deployment to one physical server.
A more flexible approach is to add a custom label.
For example:
kubectl label node worker-node-01.example.local \
workload-group=application-stack
Then use:
nodeSelector:
workload-group: application-stack
This is easier to maintain.
If the workload later needs to move to a different node, remove the label from the old node and add it to the new one.
Example:
kubectl label node worker-node-01.example.local \
workload-group-
Then:
kubectl label node worker-node-02.example.local \
workload-group=application-stack
The YAML file does not need to change.
Use Node Affinity for More Flexible Rules
nodeSelector supports only exact label matching.
For more advanced placement rules, Kubernetes provides node affinity.
Example:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: workload-group
operator: In
values:
- application-stack
This behaves similarly to nodeSelector, but it supports richer matching logic.
Node affinity can use operators such as:
InNotInExistsDoesNotExistGtLt
It can also define preferred rules instead of mandatory rules.
Required Versus Preferred Node Affinity
A required rule means the Pod cannot run anywhere else.
Example:
requiredDuringSchedulingIgnoredDuringExecution:
A preferred rule means Kubernetes should try to use the matching node, but may choose another one if necessary.
Example:
preferredDuringSchedulingIgnoredDuringExecution:
For production systems, preferred affinity is sometimes safer because it preserves failover options.
Example Preferred Node Affinity
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: workload-group
operator: In
values:
- application-stack
This tells Kubernetes:
Prefer nodes labeled
workload-group=application-stack, but use another node if required.
This approach gives the scheduler more flexibility than a strict nodeSelector.
Do Not Confuse Node Selectors with Pod Selectors
Kubernetes uses the word selector in several places.
They are not the same thing.
Service Selector
A Service finds Pods:
selector:
app: web-application
Deployment Selector
A Deployment identifies the Pods it manages:
selector:
matchLabels:
app: web-application
Node Selector
A Pod chooses a node:
nodeSelector:
kubernetes.io/hostname: worker-node-01.example.local
A simple way to remember the difference is:
- Service selector: finds Pods;
- Deployment selector: manages Pods;
- nodeSelector: chooses nodes.
Security Considerations for Kubernetes Secrets
A typical deployment may store application and database credentials in Kubernetes Secrets.
For example:
apiVersion: v1
kind: Secret
metadata:
name: application-secrets
type: Opaque
stringData:
DB_USERNAME: application
DB_PASSWORD: change-me
Do not publish real secrets in:
- public Git repositories;
- blog articles;
- screenshots;
- support forums;
- public issue trackers;
- shared example files.
Replace sensitive values with placeholders such as:
DB_PASSWORD: "<secure-password>"
Also remember that Kubernetes Secrets are base64-encoded by default, not automatically encrypted in all cluster configurations.
For production environments, consider:
- encryption at rest;
- external secret managers;
- restricted RBAC permissions;
- sealed secrets;
- secret rotation;
- avoiding credentials directly in Git.
Common Mistakes
Mistake 1: Adding nodeSelector to the Deployment Root
Incorrect:
spec:
nodeSelector:
Correct:
spec:
template:
spec:
nodeSelector:
Mistake 2: Adding It to a Service
Incorrect:
kind: Service
spec:
nodeSelector:
A Service does not create Pods.
Mistake 3: Using the Wrong Node Name
The value must match the node label exactly.
Verify with:
kubectl get nodes --show-labels
Mistake 4: Ignoring Storage Topology
A Pod may match the node but still fail to mount its volume.
Mistake 5: Forgetting Node Failure Behavior
A strict hostname selector prevents automatic failover to another server.
Mistake 6: Placing the Selector Under a Container
Incorrect:
containers:
- name: application
nodeSelector:
nodeSelector belongs to the Pod specification, not an individual container.
Mistake 7: Assuming Each Container Needs Its Own Selector
All containers in one Pod run on the same node.
Only one selector is needed per Pod template.
Recommended Verification Checklist
Before applying the configuration, verify the following:
- The node exists.
kubectl get nodes
- The node is ready.
kubectl get node worker-node-01.example.local
- The hostname label is correct.
kubectl get node worker-node-01.example.local --show-labels
- The node has enough resources.
kubectl describe node worker-node-01.example.local
- PVCs are bound.
kubectl get pvc -n example-development
- Volumes are compatible with the selected node.
kubectl describe pv <persistent-volume-name>
- The node does not contain blocking taints.
kubectl describe node worker-node-01.example.local
- The YAML is valid.
kubectl apply --dry-run=client -f deployment.yaml
- The workloads start on the expected node.
kubectl get pods -n example-development -o wide
Final Example
For a MySQL StatefulSet:
template:
metadata:
labels:
app: mysql
spec:
nodeSelector:
kubernetes.io/hostname: worker-node-01.example.local
For an application Deployment:
template:
metadata:
labels:
app: web-application
spec:
nodeSelector:
kubernetes.io/hostname: worker-node-01.example.local
These are the only workload resources in the example that require the scheduling rule.
Secrets, ConfigMaps, Services, PVCs, and Ingress resources remain unchanged.
Conclusion
Kubernetes nodeSelector is one of the simplest ways to control where a Pod runs.
It is easy to configure and useful for:
- node-specific hardware;
- local storage;
- development environments;
- predictable workload placement;
- temporary infrastructure constraints.
The most important rule is that nodeSelector must be added to the Pod template under:
spec:
template:
spec:
It should not be added to Secrets, ConfigMaps, Services, PersistentVolumeClaims, or Ingress resources.
Although hostname-based placement is effective, it creates a strong dependency on one server. For long-term production use, custom node labels or node affinity usually provide a cleaner and more flexible design.
Before applying a strict node selector, always verify:
- the node label;
- the node capacity;
- storage compatibility;
- taints and tolerations;
- the expected behavior during node failure.
Used carefully, nodeSelector provides predictable workload placement without requiring complicated Kubernetes scheduling rules.


