Skip to content

Pinning workloads to nodes: taints, tolerations & affinity

Use the placement block to control which Kubernetes nodes each Nextcloud component lands on. Typical use cases:

  • Route managed-database pods to a dedicated, tainted database-node pool
  • Reserve a labelled Nextcloud-app pool for high-memory tenants
  • Spread app replicas across failure domains with topologySpreadConstraints

How it works

Taints live on nodes, not on workloads. You taint a node out-of-band:

kubectl taint nodes db-node-01 role=database:NoSchedule

The workload then needs a matching toleration to be scheduled there. A placement block on the CRD supplies these tolerations (and optionally nodeSelector / affinity / topologySpreadConstraints) to the corresponding Kubernetes pod.

The four placement blocks

Block What it targets
spec.placement Nextcloud app pod (and the cron pod — they share scheduling)
spec.redis.placement Redis master (and replica, when configured) — no-op for external Redis
spec.database.placement Managed PostgreSQL instances[0] — only when database.managed: true
spec.backups.placement S3 backup job pod

The cascade rules apply: a NextcloudProfile or NextcloudPool template can set a placement default, and an individual NextcloudInstance deepmerges on top (instance values win per-key). See Configuration Profiles for the full merge order.

Field reference

Each placement block accepts the same four sub-fields, all optional:

Field Type Description
nodeSelector map[string]string Exact node-label match. Fully API-server validated.
tolerations array Tolerations matching node taints. Fully typed (see below).
affinity object Full core/v1 Affinity structure, passed through to the workload.
topologySpreadConstraints array Full core/v1 TopologySpreadConstraint list, passed through.

affinity and topologySpreadConstraints are passthrough (x-kubernetes-preserve-unknown-fields): the operator does not validate their internal structure — Kubernetes does, when the pod is admitted. This matches the existing spec.backups.data.affinity precedent and avoids schema drift as Kubernetes evolves.

Toleration fields

Field Type Values
key string taint key (e.g. role)
operator string Equal (default) or Exists
value string taint value — omit when operator: Exists
effect string NoSchedule, PreferNoSchedule, or NoExecute
tolerationSeconds integer seconds before eviction (only with NoExecute)

Caveats

  • External Redis: spec.redis.placement is silently ignored when spec.redis.host is set (external Redis mode). The bitnami Redis subchart is not deployed, so there is no pod to schedule.
  • Managed Postgres only: spec.database.placement has no effect when database.managed: false. The Postgres pod is not created by the operator in that case.
  • Affinity deep-merge: When a profile sets an affinity key and the instance also sets it, the instance value replaces the profile value at the leaf level (standard deep-merge). The existing pgBouncer and pgbackrest anti-affinity rules set by the Percona operator are on separate sub-objects and are not disturbed.
  • spec.backups.placement vs legacy fields: spec.backups.data.nodeSelector, spec.backups.data.tolerations, and spec.backups.data.affinity are the older form and remain supported. When both are set, spec.backups.placement wins.

Worked example: tainted database-node pool

This example shows a single NextcloudInstance whose Nextcloud app lands on a nextcloud-pool labelled node, while the managed Postgres lands on a tainted database-pool node that other workloads cannot use.

Step 1 — taint the database nodes:

kubectl taint nodes db-node-01 db-node-02 db-node-03 \
  role=database:NoSchedule

Step 2 — apply the NextcloudInstance:

apiVersion: k8s.bnerd.com/v1alpha1
kind: NextcloudInstance
metadata:
  name: acme-corp
  namespace: nextcloud
spec:
  profile: production
  ingress:
    host: cloud.acme.example.com

  # Pin the Nextcloud app pod to the nextcloud pool
  placement:
    nodeSelector:
      node-pool: nextcloud

  # Managed Postgres — tolerate the database taint and pin to the DB pool
  database:
    managed: true
    placement:
      nodeSelector:
        node-pool: database
      tolerations:
        - key: role
          operator: Equal
          value: database
          effect: NoSchedule

  # Redis runs on the nextcloud pool alongside the app
  redis:
    enabled: true
    placement:
      nodeSelector:
        node-pool: nextcloud

  # Backup jobs also tolerate the DB taint (they need access to the data volume)
  backups:
    placement:
      nodeSelector:
        node-pool: database
      tolerations:
        - key: role
          operator: Equal
          value: database
          effect: NoSchedule
    data:
      enabled: true
      schedule: "0 3 * * *"
      deleteOnCleanup: false

After kubectl apply, verify pod placement:

# App pod
kubectl get pods -n nextcloud -l app.kubernetes.io/instance=acme-corp \
  -o wide

# Postgres instance pod
kubectl get pods -n nextcloud -l postgres-operator.crunchydata.com/cluster=acme-corp-pg \
  -o wide

Profile-level placement defaults

Set a baseline for all instances in a pool by adding placement to a NextcloudProfile or NextcloudPool template:

apiVersion: k8s.bnerd.com/v1alpha1
kind: NextcloudProfile
metadata:
  name: database-node-pool
spec:
  defaults:
    placement:
      nodeSelector:
        node-pool: nextcloud
    database:
      placement:
        nodeSelector:
          node-pool: database
        tolerations:
          - key: role
            operator: Equal
            value: database
            effect: NoSchedule

Individual instances using this profile inherit the defaults and can deepmerge additional fields — for example, adding a topologySpreadConstraint without having to repeat the toleration.