Running Kubernetes Workloads on a Specific Node Using nodeSelector

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:

  • Deployment
  • StatefulSet
  • DaemonSet
  • Job
  • CronJob
  • standalone Pod

It does not belong directly in resources such as:

  • Secret
  • ConfigMap
  • Service
  • PersistentVolumeClaim
  • Ingress
  • Namespace

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:

  • In
  • NotIn
  • Exists
  • DoesNotExist
  • Gt
  • Lt

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:

  1. The node exists.
kubectl get nodes
  1. The node is ready.
kubectl get node worker-node-01.example.local
  1. The hostname label is correct.
kubectl get node worker-node-01.example.local --show-labels
  1. The node has enough resources.
kubectl describe node worker-node-01.example.local
  1. PVCs are bound.
kubectl get pvc -n example-development
  1. Volumes are compatible with the selected node.
kubectl describe pv <persistent-volume-name>
  1. The node does not contain blocking taints.
kubectl describe node worker-node-01.example.local
  1. The YAML is valid.
kubectl apply --dry-run=client -f deployment.yaml
  1. 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.

 

This article is inspired by real-world challenges we tackle in our projects. If you're looking for expert solutions or need a team to bring your idea to life,

Let's talk!

    Please fill your details, and we will contact you back

      Please fill your details, and we will contact you back