Skip to content

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, optional zone and replication address, thinPoolSize for shared VGs. See the Quickstart layouts.
  • storageClasses — the classes to create: replicas, quorum policy (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