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:
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.placementis silently ignored whenspec.redis.hostis set (external Redis mode). The bitnami Redis subchart is not deployed, so there is no pod to schedule. - Managed Postgres only:
spec.database.placementhas no effect whendatabase.managed: false. The Postgres pod is not created by the operator in that case. - Affinity deep-merge: When a profile sets an
affinitykey and the instance also sets it, the instance value replaces the profile value at the leaf level (standard deep-merge). The existingpgBouncerandpgbackrestanti-affinity rules set by the Percona operator are on separate sub-objects and are not disturbed. spec.backups.placementvs legacy fields:spec.backups.data.nodeSelector,spec.backups.data.tolerations, andspec.backups.data.affinityare the older form and remain supported. When both are set,spec.backups.placementwins.
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:
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.