Once your pools carry [labels](/docs/hetzner/apalla/servers-and-nodes/pools/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 one rule that trips people up: 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/pools/label-nodes-and-assign-roles) for the full rule. ## Combine pool labels with system labels Syself also sets labels on every node before your pool's labels land. You can select on these without declaring them anywhere: | Label | Value | Where | | --------------------------------------- | --------------------------------------------- | ---------------- | | `autopilot.syself.com/machine-type` | `baremetal` or `hcloud` | every node | | `instance.hetzner.cloud/is-root-server` | `"true"` | bare-metal nodes | | `node.kubernetes.io/instance-type` | for example `cpx42` | every node | | `topology.kubernetes.io/region` | location on cloud, network zone on bare metal | every node | | `topology.kubernetes.io/zone` | datacenter on cloud, location on bare metal | every node | | `autopilot.syself.com/gpu` | `"true"` | GPU nodes | | `kubernetes.io/hostname` | the node's hostname | every node | `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 ``` See [Node labels and annotations](/docs/hetzner/apalla/reference/node-labels-and-annotations) for the full list. ## 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, use node affinity and pod anti-affinity. Both live on [Set affinity and anti-affinity](/docs/hetzner/apalla/workloads/placement/affinity-and-anti-affinity), which owns required-versus-preferred node affinity and one-replica-per-node anti-affinity for databases and quorum members. To spread replicas evenly as your node count changes, reach for [topology spread constraints](/docs/hetzner/apalla/workloads/placement/topology-spread-constraints) instead. The one thing that stays on this page: affinity rules read the same node labels a `nodeSelector` does, so the exact-key gotcha above and the system-label table apply 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. A label alone does not reserve a pool. The supported way to reserve one is to give every other workload a different pool to select, so nothing else lands there. Syself Autopilot does not support node taints; see [Node taints are not supported](/docs/hetzner/apalla/servers-and-nodes/scheduling/apply-taints).