Once your pools carry [labels](/docs/hetzner/apalla/servers-and-nodes/labels/label-nodes-and-assign-roles), you can steer pods onto them. Use `nodeSelector` for the simple case, and node affinity when you need conditions. The pod's selector must match the label's **exact key and value**. ## nodeSelector for the simple case `nodeSelector` pins a pod to nodes that carry every label listed: ```yaml spec: nodeSelector: node-role.kubernetes.io/backend: "" ``` ```yaml spec: nodeSelector: autopilot.syself.com/machine-type: baremetal ``` ## Match the exact key and empty-string value Role labels under `node-role.kubernetes.io/` use an **empty-string value** (`""`), not `"true"`. A selector for `"true"` does not match a node labelled `""`: ```yaml # Node label set by the pool: node-role.kubernetes.io/backend: "" nodeSelector: node-role.kubernetes.io/backend: "true" node-role.kubernetes.io/backend: "" ``` > [!NOTE] > Only labels in the `node-role.kubernetes.io`, `node-restriction.kubernetes.io`, and `node.cluster.x-k8s.io` domains reach the node from a pool definition. Any other label is silently dropped, so a selector for it never matches. See [Label nodes and assign roles](/docs/hetzner/apalla/servers-and-nodes/labels/label-nodes-and-assign-roles) for the full rule. ## Combine pool labels with system labels Syself Autopilot also sets labels on every node before your pool's labels land, and you can select on those without declaring them anywhere. See [Platform labels and annotations](/docs/hetzner/apalla/servers-and-nodes/labels/platform-labels-and-annotations) for the full list. `nodeSelector` requires every listed label to match. Combine one of your pool's labels with a system label to narrow further. This pod schedules only on a node that is in the `backend` pool **and** bare metal: ```yaml spec: nodeSelector: node-role.kubernetes.io/backend: "" autopilot.syself.com/machine-type: baremetal ``` ## Need rules, not just labels `nodeSelector` matches labels and stops there. When you need a hard-or-soft choice, matching operators like `In` or `Exists`, or placement relative to other pods, see [Set affinity and anti-affinity](/docs/hetzner/apalla/workloads/placement/affinity-and-anti-affinity). To spread replicas evenly as your node count changes, see [topology spread constraints](/docs/hetzner/apalla/workloads/placement/topology-spread-constraints). Affinity rules read the same node labels a `nodeSelector` does, so the exact-key rule above applies to them too. ## Why unmatched pods go elsewhere A selector or affinity rule **attracts**. It does not repel. A pod that sets no selector can still land on your labelled nodes. A pod whose preferred affinity cannot be met just schedules somewhere else. ## Reserve a pool for one workload To hold a class of hardware such as a GPU, a bare-metal server, or a client's dedicated capacity for one workload, use a pool label instead of a taint: 1. Put the dedicated servers in their own [worker pool](/docs/hetzner/apalla/servers-and-nodes/pools/node-pools-overview) and set a label in the pool's `metadata`. A pool label persists across node replacement; a `kubectl` label does not. 2. Point the workload at that pool with a `nodeSelector`, or with [node affinity](/docs/hetzner/apalla/workloads/placement/affinity-and-anti-affinity) when you need a rule rather than a plain match. 3. Do not deploy anything else that selects the pool. You control what runs there. ```yaml # The dedicated workload targets the pool's label spec: nodeSelector: autopilot.syself.com/gpu: "true" ``` > [!NOTE] > A label reliably pulls the **wanted** pod onto the dedicated nodes and survives node replacement, but it cannot keep other pods off, which is something a taint would do. In practice this rarely matters: you control what you deploy, and scarce hardware is guarded by its own resource accounting (a GPU through [`nvidia.com/gpu`](/docs/hetzner/apalla/workloads/specialized/run-gpu-workloads), a secure-runtime pod through its [runtime class](/docs/hetzner/apalla/workloads/secure/run-in-the-secure-runtime)), so an unmarked pod that lands there only uses CPU and memory, never the GPU or secure runtime you had reserved for other workloads. For a boundary that does not depend on every pod carrying the right selector, a dedicated bare-metal pool is the hard separation; see [Dedicate node pools per client](/docs/hetzner/apalla/servers-and-nodes/fleet/dedicate-node-pools-per-client). For a hard, enforced guarantee that nothing else can schedule onto a set of nodes, contact Syself. ## Related - [Label nodes and assign roles](/docs/hetzner/apalla/servers-and-nodes/labels/label-nodes-and-assign-roles) - [Taints and tolerations](/docs/hetzner/apalla/workloads/placement/taints-and-tolerations) - [Set affinity and anti-affinity](/docs/hetzner/apalla/workloads/placement/affinity-and-anti-affinity) - [Platform labels and annotations](/docs/hetzner/apalla/servers-and-nodes/labels/platform-labels-and-annotations)