Helm chart values¶
Every chart value is documented value-by-value in the generated
chart README
(kept in sync with values.yaml by helm-docs; CI fails if it goes
stale). This page is the orientation layer: which groups of values
exist and where their behavior is explained.
nodes— the per-node storage topology: backend (lvmthin/zfs/loopfile), device or dataset, optionalzoneand replicationaddress,thinPoolSizefor shared VGs. See the Quickstart layouts.storageClasses— the classes to create:replicas,quorumpolicy (Replication and quorum),fsType,reclaimPolicy,allowRemoteVolumeAccess(Remote consumers),isDefault.volumeSnapshotClasses— snapshot classes (Quickstart).drbd— replication tuning:portBase(Coexistence),onIoError, resync knobs,verify.algorithm/verify.schedule(verification),autoTieBreaker,autoDiskfulAfter(auto-diskful).gateway— the per-RWX-volume NFS gateway:enabled(RWX is opt-in, off by default) and the gateway image (ReadWriteMany).monitoring— PodMonitor, PrometheusRule, dashboards (Monitoring).agent/controller/sidecars— workload knobs: images, resources,agent.kubeletDir,sidecars.healthMonitor.logging— level and encoder for both components.
The complete values.yaml¶
The block below is the chart's real charts/miroir/values.yaml,
pulled in at build time (MkDocs snippets), so the documented defaults
can never drift from the file Helm actually renders.
# -- Override the chart name used in labels and default object names.
nameOverride: ""
# -- Override the fully qualified name prefix of every rendered object.
fullnameOverride: ""
# Fleet-wide defaults (kopiur layout). imagePullSecrets and commonLabels
# reach every pod/object; the scheduling globals apply to the controller —
# the only freely schedulable workload (the agent DaemonSet must run on
# every schedulable node for the CSI node service, and the setup Job is
# node-pinned).
global:
# -- Pull secrets added to every pod (controller, agent, setup, uninstall).
imagePullSecrets: []
# -- Labels stamped on every rendered object (fleet-wide labelling).
commonLabels: {}
# -- Controller scheduling defaults.
nodeSelector: {}
tolerations: []
affinity: {}
# Both processes bind the controller-runtime zap flags.
logging:
# -- Log level: debug | info | error (or any zapcore level).
level: info
# -- Encoder: json (structured, default) or console (human-readable).
format: json
# =============================================================================
# Controller (root = controller, kopiur layout; components nest below)
# =============================================================================
# -- Controller image (distroless, no storage userland — the controller never
# execs a storage CLI).
image:
repository: ghcr.io/home-operations/miroir-controller
# Overrides the image tag; defaults to the chart appVersion so the chart and
# image versions stay in lock-step (set to e.g. "main" to track the rolling tag).
tag: ""
# Pins the image by digest (sha256:…); when set it takes precedence over the tag.
# The release pipeline fills this with the published image's digest.
digest: ""
pullPolicy: IfNotPresent
# -- system-cluster-critical protects the single controller from eviction
# under node pressure — while it is down, no volume can be provisioned,
# expanded, or snapshotted.
priorityClassName: system-cluster-critical
# -- Wait for agents to realise a new volume. Keep sidecars.*.timeout at or
# above this, or the sidecar RPC deadline fires before this one and the
# knob has no effect.
provisionTimeout: 120s
# -- Thin-provisioning overcommit guardrail: CreateVolume is refused when a
# node's provisioned total would exceed capacity × this ratio. 2× is the
# classic CoW headroom; raise it only if you trust your usage to stay
# sparse, lower it toward 1 to provision conservatively.
overcommitRatio: 2
# -- Add a diskless tie-breaker replica to 2-replica freeze volumes when a
# spare storage node exists, so majority quorum survives a single node
# loss. Also retrofits existing freeze volumes at controller startup.
autoTieBreaker: true
# -- Convert a diskless leg (client or tie-breaker) that has stayed DRBD
# Primary past this duration into a local diskful replica on its node, so a
# settled consumer stops paying network I/O (LINSTOR's auto-diskful; Go
# duration, e.g. "10m"). Conversion needs the leg's node in `nodes` with
# fresh pool stats and room for the volume's full size. Empty disables it.
# See the root README, "Auto-diskful".
autoDiskfulAfter: ""
# Storage-capacity-aware scheduling. When enabled, the external-provisioner
# publishes CSIStorageCapacity objects from the driver's GetCapacity RPC
# (capacity × overcommitRatio − provisioned, per storage node) and the
# kube-scheduler steers a WaitForFirstConsumer pod onto a node whose pool can
# hold the volume — instead of landing there and having CreateVolume refuse it
# and reschedule. CreateVolume's own overcommit guard stays the authority.
# Off by default: it is a behavior change — with storageCapacity on, the
# scheduler treats a (node, class) pair with no published capacity as unfit, so
# a freshly installed cluster waits for agents to publish pool stats (up to
# agent.poolStatsInterval) before the first pod schedules.
storageCapacity:
enabled: false
# -- Controller replicas. Anything above 1 automatically enables leader
# election: the extras are warm standbys (failover is lease expiry, ~15s,
# instead of a full pod reschedule), the rollout strategy switches to
# RollingUpdate, and a PodDisruptionBudget keeps one replica through
# voluntary disruptions. Pointless on a single-node cluster (the node is
# the failure domain); pair with global.affinity (pod anti-affinity) so
# replicas land on different nodes.
replicaCount: 1
# Leader election turns on by itself when replicaCount > 1 — the
# controller and each CSI sidecar then elect through coordination.k8s.io
# Leases — so this key only customises it.
leaderElection:
# -- Elect even with a single replica (replicaCount > 1 elects regardless;
# this can never switch election off above one replica).
enabled: false
# -- Lease name; empty derives the release-scoped controller name so two
# releases in one namespace never share a Lease. Keep it stable across
# upgrades.
id: ""
# -- Controller resources.
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 128Mi
# -- Extra labels on the controller pod.
podLabels: {}
# -- Extra annotations on the controller pod.
podAnnotations: {}
# -- Extra arguments for the controller container.
extraArgs: []
# -- Extra environment variables for the controller container.
extraEnv: []
# =============================================================================
# CSI sidecars (upstream sig-storage images)
# =============================================================================
sidecars:
provisioner:
image: registry.k8s.io/sig-storage/csi-provisioner:v6.3.0
# LVM/ZFS provisioning on a cold node can exceed the 10s sidecar
# default; the driver returns DeadlineExceeded and the provisioner
# retries, but a roomier timeout avoids churn.
timeout: 120s
snapshotter:
image: registry.k8s.io/sig-storage/csi-snapshotter:v8.6.0
# CreateSnapshot returns fast (readiness is polled), so the default
# suffices; raise if snapshot RPCs are slow.
timeout: 120s
resizer:
image: registry.k8s.io/sig-storage/csi-resizer:v2.2.1
# Online grow can block on a rebooting node; keep >= provisionTimeout.
timeout: 120s
# external-health-monitor-controller: polls ControllerGetVolume and raises a
# PVC event when a volume reports split-brain/degraded/disk-failed (the same
# health the agents fold into status.phase). Off by default — the
# miroir_volume_* metrics already carry this; enable for native PVC events.
healthMonitor:
enabled: false
image: registry.k8s.io/sig-storage/csi-external-health-monitor-controller:v0.18.0
# How often the monitor re-checks every volume's condition.
interval: 1m
# =============================================================================
# Agent (per-node DaemonSet: storage userland + CSI node service)
# =============================================================================
agent:
# Agent image (Debian + the lvm/zfs/drbd/mkfs userland the agent and the
# setup Job exec on each storage node).
image:
repository: ghcr.io/home-operations/miroir-agent
# Overrides the image tag; defaults to the chart appVersion.
tag: ""
# Pins the image by digest (sha256:…); takes precedence over the tag.
# The release pipeline fills this with the published image's digest.
digest: ""
pullPolicy: IfNotPresent
# Per-node backend selection comes from node annotations:
# miroir.home-operations.com/backend: lvmthin | zfs
# miroir.home-operations.com/device: /dev/disk/by-partlabel/r-miroir (lvmthin)
# miroir.home-operations.com/zfs-dataset: data-pool/miroir (zfs)
# Nodes also need the label miroir.home-operations.com/storage: "true" to receive replicas.
#
# How often each agent republishes its pool capacity to its MiroirNode
# (read by the controller for capacity-aware placement).
poolStatsInterval: 60s
# -- Concurrent volume reconciles per agent. Per-volume work is
# serialized by controller-runtime regardless; this bounds how many
# distinct volumes one agent works at once.
volumeWorkers: 4
# kubelet plugin-registration sidecar riding the agent DaemonSet.
registrar:
image: registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.17.0
# -- Kubelet root on the nodes; CSI sockets and mounts hang off it.
kubeletDir: /var/lib/kubelet
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 128Mi
# -- Extra labels on the agent pods.
podLabels: {}
# -- Extra annotations on the agent pods.
podAnnotations: {}
# -- Extra arguments for the agent container.
extraArgs: []
# -- Extra environment variables for the agent container.
extraEnv: []
# =============================================================================
# Gateway (per-RWX-volume NFS share manager)
# =============================================================================
# The controller spawns one gateway Deployment per RWX (ReadWriteMany)
# volume: it mounts the volume's device on a replica node and exports it
# over NFSv4 (userspace NFS-Ganesha), which consumers on any node mount.
# The image is the agent userland plus NFS-Ganesha.
gateway:
# -- Serve ReadWriteMany (and ReadOnlyMany) PVCs via per-volume NFS
# gateways. Opt-in: gateway pods run privileged in the release namespace,
# and any user who can create a PVC can cause one to be spawned, so
# enabling RWX is an explicit operator decision. While disabled the
# controller rejects RWX at provision time with a clear message, and the
# gateway ServiceAccount, RBAC, PodMonitor, and export alerts are not
# installed.
enabled: false
image:
repository: ghcr.io/home-operations/miroir-gateway
# Overrides the image tag; defaults to the chart appVersion.
tag: ""
# Pins the image by digest (sha256:…); takes precedence over the tag.
# The release pipeline fills this with the published image's digest.
digest: ""
pullPolicy: IfNotPresent
# =============================================================================
# Storage topology
# =============================================================================
# Per-node storage topology — the single source of truth for which nodes
# hold volumes and how. Rendered into the miroir-nodes ConfigMap, read by
# the controller (placement) and each agent (backend selection).
# Optional per-node `zone` (rack, host group, AZ): when set, the controller
# spreads a volume's replicas across distinct zones; empty is unconstrained.
# Optional per-node `address` (IPv4 or IPv6) pins DRBD replication to a
# dedicated storage NIC/VLAN; empty uses the node's InternalIP. It applies
# to volumes created afterwards — existing volumes keep the address
# persisted at creation.
# nodes:
# kharkiv:
# backend: lvmthin
# device: /dev/disk/by-partlabel/r-miroir
# # thinPoolSize: 400g # bound the pool when sharing the VG
# # zone: rack-1 # spread replicas across failure domains
# # address: 10.0.100.11 # replicate over a dedicated storage NIC
# paris:
# backend: zfs
# zfsDataset: data-pool/miroir
# # zone: rack-2
# le-havre:
# # loopfile: sparse files on the node's existing filesystem — no
# # dedicated disk or pool. baseDir must be reflink-capable (XFS
# # reflink=1, e.g. Talos /var, or btrfs) for CoW snapshots; the agent
# # refuses to start otherwise. The dir is hostPath-mounted into the
# # agent at the same path.
# backend: loopfile
# baseDir: /var/lib/miroir
nodes: {}
# =============================================================================
# Storage classes
# =============================================================================
# -- StorageClasses to create. Empty by default: declare the classes you
# want. One local + one replicated is the common pair (see the example
# below). Per entry:
# name (required) the StorageClass name
# replicas replica count, default 1; >1 makes it DRBD-replicated
# quorum freeze | last-man-standing (replicated only, default
# freeze). freeze never diverges but halts writes without a
# peer majority; last-man-standing keeps the survivor
# writable at the risk of split-brain. See the root README,
# "Replication and quorum".
# fsType ext4 | xfs, default ext4
# allowRemoteVolumeAccess
# true | false (replicated only; the controller defaults
# absent to true, matching LINSTOR): pods on nodes without
# a replica consume the volume through an ephemeral
# diskless DRBD leg at replication-network speed. Set
# false to pin pods to replica nodes for local reads. See
# the root README, "Remote consumers".
# bitmapGranularity
# DRBD bitmap block size in bytes (replicated only): a
# power of two, 4096–1048576, default absent (DRBD's 4096).
# Each dirty bit tracks this many bytes — coarser cuts
# bitmap RAM proportionally (65536 ≈ 1/16th) but resyncs
# more per dirty bit; worth considering for classes holding
# large volumes. Fixed when a replica's metadata is
# created: changing the class affects new volumes only.
# reclaimPolicy Delete | Retain, default Delete
# isDefault set the cluster default-class annotation, default false
# Example (coexisting with OpenEBS, which stays the cluster default):
# storageClasses:
# - name: miroir-local
# replicas: 1
# - name: miroir-replicated
# replicas: 2
# quorum: freeze
storageClasses: []
# Requires the cluster-wide snapshot-controller + CRDs (deployed
# separately; kube-system/snapshot-controller in the homelab).
# -- VolumeSnapshotClasses to create (requires the snapshot-controller +
# CRDs, deployed separately). Empty by default. Per entry:
# name (required) the VolumeSnapshotClass name
# deletionPolicy Delete | Retain, default Delete
# isDefault set the cluster default-snapshot-class annotation,
# default false
# Example:
# volumeSnapshotClasses:
# - name: miroir-snap
# deletionPolicy: Delete
volumeSnapshotClasses: []
# =============================================================================
# DRBD replication tuning
# =============================================================================
# DRBD replication tuning applied cluster-wide through the common{} section of
# global_common.conf; every resource inherits it. Leave a value empty to use
# DRBD's built-in default. Tune to your replication network — see the LINBIT
# "Tuning the DRBD Resync Controller" guide.
drbd:
# @schema
# type: integer
# minimum: 1024
# maximum: 64000
# @schema
# -- Lowest TCP port for DRBD replication links, one per replicated volume
# ascending (7000, 7001, …). The agent runs hostNetwork so these bind on
# the node's kernel. Ceph mgr dashboard's non-SSL default is also 7000;
# co-locating with Rook host-network Ceph requires moving one of them
# (see issue #148). Existing volumes keep their assigned ports.
portBase: 7000
# How a diskful replica reacts to a backing-device I/O error. detach
# (the LINSTOR/DRBD-recommended default) drops the failing leg to
# Diskless and serves reads/writes via the peer instead of surfacing
# EIO into the pod. Set to pass_on to propagate errors instead.
onIoError: detach
# @schema
# type: [integer, string]
# @schema
# -- al-extents, the DRBD activity-log size (number of 4 MiB extents kept
# "hot"). DRBD's default (1237) forces frequent metadata updates under a
# scattered random-write workload; raising it (e.g. 6007) cuts that write
# amplification at the cost of a longer resync of the active region after a
# crash. Empty leaves DRBD's default. Must be a prime below 65534.
alExtents: ""
resync:
# -- c-plan-ahead in 0.1s units; a value > 0 enables DRBD's variable-rate resync controller.
planAhead: ""
# -- c-fill-target, the resync controller's target fill level (e.g. "1M").
fillTarget: ""
# -- c-max-rate, the resync bandwidth ceiling used when the link is idle (e.g. "720M").
maxRate: ""
# -- c-min-rate, the resync floor guaranteed even under application I/O.
# Defaulted to 10M: DRBD's kernel default (250 KiB/s) leaves a degraded
# volume resyncing for days under load; 10 MiB/s heals a 100Gi leg in
# hours while still yielding most of a 1GbE link to applications. Lower
# on a slow shared link.
minRate: 10M
# -- resync-rate, the fixed rate used only when the controller is off (planAhead empty or 0).
rate: ""
# -- rs-discard-granularity cluster-wide fallback: during a full resync,
# runs of zeroes are sent as discards of this size instead of written
# out (e.g. "65536"), keeping a re-added thin leg thin. Normally leave
# empty — the agent probes each lvmthin/zfs backing device and renders
# an exact per-leg value that overrides this (loopfile is never probed:
# loop devices mishandle it, so also leave this empty on clusters with
# loopfile-backed replicated volumes).
discardGranularity: ""
net:
# -- max-buffers, the DRBD receive-buffer count (e.g. "36864"); raises
# resync throughput on fast links.
maxBuffers: ""
verify:
# -- verify-alg, arming `drbdadm verify <res>` — the only cross-leg
# integrity check (a zfs scrub only validates one leg against itself).
# Defaulted to crc32c: drbd.ko depends on libcrc32c so it is present on
# every node, and it costs nothing until a verify runs. Empty disables
# verification, including the schedule below.
algorithm: crc32c
# -- Cron spec (5-field, agent-local/UTC time) for a scheduled online
# verify of every replicated volume. The agent initiates it once per
# volume from the coordinator (first diskful replica), serialized per
# node, skipping volumes that are resyncing or already verifying.
# Findings land in the volume's status (`lastVerifyOutOfSyncBytes`), the
# `miroir_volume_verify_*` metrics, and a `VerifyOutOfSync` event. Empty
# = no scheduled verify (run it by hand). Requires `algorithm` set.
schedule: ""
# -- Pause scheduled verify without dropping the schedule above.
suspend: false
# =============================================================================
# Monitoring
# =============================================================================
# Requires the Prometheus Operator CRDs (deployed separately).
monitoring:
podMonitor:
# -- Create a Prometheus Operator PodMonitor (requires its CRDs) scraping
# the controller and every agent pod on their metrics ports. The
# per-volume miroir_volume_* gauges are exported by the agents.
enabled: false
# -- Scrape interval.
interval: 30s
# -- Scrape timeout.
scrapeTimeout: 10s
# -- Metrics path.
path: /metrics
# -- PodMonitor labels.
labels: {}
# -- PodMonitor annotations.
annotations: {}
# -- Prometheus metric relabelings.
metricRelabelings: []
# -- Extra Prometheus relabelings (applied before scraping); a node
# label from the pod's node name is always added.
relabelings: []
# -- Pod target labels to copy from pods.
podTargetLabels: []
prometheusRule:
# -- Create a PrometheusRule with alerting rules (requires the Prometheus Operator CRDs).
enabled: false
# -- PrometheusRule annotations.
annotations: {}
# -- PrometheusRule labels.
labels: {}
# -- Extra labels added to every alert rule.
additionalRuleLabels: {}
# -- Extra annotations added to every alert rule.
additionalRuleAnnotations: {}
# -- Days since the last completed scheduled verify before
# MiroirVolumeVerifyStale fires. Size it to just over the schedule
# period (a weekly `drbd.verify.schedule` → 8). The rule is only
# rendered when `drbd.verify.schedule` is set.
verifyStaleDays: 8
dashboards:
# -- Render the Grafana dashboard ConfigMap (for grafana-operator or the kube-prometheus-stack sidecar).
enabled: false
# -- Namespace for the dashboard objects; defaults to the release namespace.
namespace: ""
# -- Annotations added to the dashboard ConfigMap.
annotations: {}
# -- Labels added to the dashboard ConfigMap.
labels: {}
grafanaOperator:
# -- Render a GrafanaDashboard CR (grafana-operator) instead of a sidecar ConfigMap.
enabled: false
# -- If true allows for a Grafana in any namespace to access this GrafanaDashboard.
allowCrossNamespaceImport: true
# -- Folder to create the dashboard in.
folder: ""
# -- Resync period for the Grafana operator to check for updates to the dashboard.
resyncPeriod: "10m"
# -- Selected labels for Grafana instance.
matchLabels: {}
# =============================================================================
# Uninstall hook
# =============================================================================
# Pre-delete hook Job: deletes miroirsnapshots/miroirvolumes (so the agent
# tears down DRBD/LVM) before the CRDs are released. Needs only `kubectl`.
uninstall:
image: registry.k8s.io/kubectl:v1.36.2