VeltrixDB Docs
v1.16 GitHub FAQ Quickstart

Kubernetes Operator

The VeltrixDB Kubernetes Operator manages VeltrixCluster custom resources — provisioning the StatefulSet, NVMe storage, and networking, and reconciling scale, upgrade, and recovery events without manual intervention.

Maintained in a separate repository. The Operator lives in VeltrixDB-Kubernetes-Operator, not in the main veltrixdb repository, and is versioned separately. The CRD fields, defaults, status values and reconcile behaviour on this page were taken from that repository and have not been re-verified for server v1.12 — treat api/v1alpha1/veltrixcluster_types.go and controllers/veltrixcluster_controller.go in the version you deploy as the source of truth. Statements about what the server does with the flags the Operator passes are checked against the main repository and are called out below.

Overview

The Operator is built with Kubebuilder and watches a single CRD, VeltrixCluster (group veltrix.io/v1alpha1, short name vc). On each reconcile pass it creates or updates the child resources needed to run a cluster: a StorageClass, an NVMe-provisioning DaemonSet, a ServiceAccount, headless and client Service objects, a ConfigMap, a ServiceMonitor (best-effort, if the Prometheus Operator CRDs are installed), and the StatefulSet itself.

Prerequisites

RequirementDetail
Kubernetes1.27+
Go1.21+ (only needed if building the operator from source)
NodesLocal NVMe SSDs — see the per-cloud notes below
GKENode pool created with --local-ssd-interface=NVME
AWSInstance types with local NVMe storage: i3, i3en, i4i, im4gn, is4gen
AzureLsv2/Lsv3 series with local NVMe storage
GKE requirement Create node pools with --local-ssd-interface=NVME. Without it, GKE merges all local SSDs into a single RAID-0 device and you lose the parallel I/O benefit that VeltrixDB's per-disk WAL/VLog layout depends on.

Installation

From a checkout of the Operator repository, install the CRDs, then deploy the operator's controller manager:

git clone https://github.com/VeltrixDB/VeltrixDB-Kubernetes-Operator.git
cd VeltrixDB-Kubernetes-Operator

# 1. Install the CustomResourceDefinitions
kubectl apply -f config/crd/bases/

# 2. Deploy the operator (controller manager Deployment + RBAC)
kubectl apply -f config/manager/manager.yaml

Before creating any VeltrixCluster, deploy the NVMe mount-keeper DaemonSet so real XFS filesystems are bind-mounted at the PV source paths before kubelet bind-mounts them into the veltrixdb pod. Skipping this step causes ENOSPC on the very first WAL write.

kubectl apply -f config/manager/nvme-mount-keeper.yaml
kubectl rollout status daemonset/nvme-mount-keeper -n veltrixdb

See NVMe Disk Provisioning for why the keeper is required on GKE COS, AWS Bottlerocket, and any OS where kubelet runs inside its own mount namespace.

VeltrixCluster spec reference

The fields below were read from the VeltrixClusterSpec Go type (api/v1alpha1/veltrixcluster_types.go, Operator repo; not re-checked for this release). Defaults marked operator default are filled in by the reconcile loop's applyDefaults step when the field is left unset in the CR.

Core

FieldTypeDefaultDescription
sizeint323Number of VeltrixDB nodes. Each node owns a contiguous shard range. Minimum 1.
versionstring1.0.0Container image tag. The default is an old release: set it explicitly to the server version you run.
imagestringghcr.io/veltrixdb/veltrixdbContainer image repository (combined with version to form the full image reference).
replicationFactorint323Replica copies per shard (min 1, max 5). The operator reports a ReplicationFactorMet condition when live replicas fall below this. The server itself has no replication-factor flag (replicated mode uses a fixed RF of 3), so this is a status threshold, not a server setting.
cacheMaxSizeMBint3265536LIRS cache size in MiB, passed as -cache. Minimum 256.
storageClassNamestringlocal-nvmeStorageClass used for NVMe PVCs — must match the class the provisioner creates.
disksPerNodeint328NVMe disks (and PVCs) per pod. Min 1, max 16.
diskSizequantity375GiCapacity requested per PVC.
resourcesResourceRequirements—Standard Kubernetes compute resource requests/limits for the veltrixdb container.

Networking

FieldDefaultDescription
gossipPort7946Gossip port. Server fact: gossip is TCP, and peers always dial the derived address (client port + 3, i.e. 9003 with the default serverPort) — a different listen port does not change where peers connect. Leave the server on its derived ports and open client port + 1/2/3/5 between pods.
serverPort9000TCP port for the binary/text protocol.
metricsPort2112Prometheus metrics, /healthz, /readyz.
tlsPort9443TLS listener; only active when tlsSecretName is set. The plain serverPort stays up for internal cluster traffic.
(not in the CRD)No field for inter-node TLS (-cluster-tls-*, -cluster-mtls) or the cluster secret (-cluster-secret-file / VELTRIXDB_CLUSTER_SECRET, v1.11) is documented. Without one of them the server's transfer listener is unauthenticated — supply the secret through pod env if your Operator version cannot.

Scheduling

FieldDefaultDescription
podAntiAffinityTopologyKeykubernetes.io/hostnameTopology key for required pod anti-affinity — one VeltrixDB pod per physical host.
tolerations—Applied verbatim to the StatefulSet pod template.
nodeSelector—Constrains pod scheduling to matching nodes.
imagePullSecrets—For private registries.

Storage engine tuning

FieldDefaultDescription
numShards1024Has no effect on the server: the index always uses its compile-time shard count (8192; StorageConfig.NumShards is vestigial) and the server has no shard flag. Leave it at the default.
keyValueSeparationtrueEnables WiscKey-style KV separation — only key metadata lives in SSTables; values append to a per-disk VLog.
vlogGCThreshold"0.40" (operator default; CRD doc states 0.30)Decimal string in [0.0, 1.0]. VLog garbage fraction that triggers defragmentation (server flag -gc-threshold, server default 0.30).
lirRatio"0.90"Fraction of the LIRS cache reserved for the high-locality (LIR) resident set.
autoTunefalseWhen true, the binary runs hardware detection at startup and derives cache size, shard count, compaction threads, and I/O mode itself; the operator omits -cache and passes -auto-tune instead.
sysTunetrueHost-level tuning (sysctl, hugepages, CPU governor, IRQ balance, RLIMIT_NOFILE) via hardware.SysTune. Linux only; only meaningful when autoTune is true. Requires CAP_SYS_ADMIN or root.
checkpointIntervalMs0 (disabled)WAL checkpoint interval. When non-zero, passed as -wal-checkpoint-interval-ms. Keep it at 0: the current server defines no such flag, and Go's flag parser makes it exit at startup with "flag provided but not defined". A clean shutdown always writes a compacted checkpoint (wal.log.ckpt, renamed over wal.log).

WAL / VLog flush

FieldDefaultDescription
walFlushWindowMs10WAL group-commit flush window (ms). Must equal vlogFlushWindowMs. Overrides the server's own default of 15 ms. Since v1.12 the server's adaptive group commit (-group-commit=adaptive, default) treats the window as an upper bound: a lone writer is synced at once.
vlogFlushWindowMs10VLog fdatasync group-commit window (ms). WAL and VLog are submitted concurrently; effective P99 is the max of the two.
walMaxBatchEntries4096Caps the WAL group-commit batch size before the flush timer fires early. Min 1, max 65536.

Security

FieldDescription
tlsSecretNameName of a kubernetes.io/tls Secret with tls.crt/tls.key. When set, the binary is started with -tls-addr, -tls-cert, -tls-key.
tlsCASecretNameSecret containing ca.crt for mTLS client verification; passed as -tls-ca alongside tlsSecretName.
authConfigSecretSecret with key auth-config.json containing RBAC user definitions. Passed as -auth-config. Permission bitmask: PermRead=1, PermWrite=2, PermDelete=4, PermAdmin=8.

Raw NVMe block-device VLog

Set rawVLog to write the VLog directly to /dev/nvmeXnY block devices, skipping the filesystem for the value-bytes path. WAL and segment files still use the per-pod XFS PVCs.

FieldDefaultDescription
rawVLog.enabledfalseWhen true, the operator adds SYS_RAWIO to the container's capability set, mounts /dev as a HostPath volume, passes -raw-vlogs to the binary, sets seccompProfile: Unconfined and an unconfined AppArmor annotation, and runs the container privileged: true as root (required to pass the cgroup device allowlist on GKE COS).
rawVLog.devices[]Explicit block-device list, paired index-by-index with the per-pod NVMe PVCs. When empty, the operator infers /dev/nvme0n1 … /dev/nvme(disksPerNode-1)n1 (or the p2 partition suffix when xfsPartitionPercent > 0) — the GKE local-NVMe convention.
rawVLog.xfsPartitionPercent0 (whole-disk XFS)Percentage of each NVMe disk reserved as an XFS partition for WAL/segments; the remainder stays a raw partition for VLog values. Min 5, max 50; typical production value 20.
Provisioning gotcha Devices intended for raw VLog use must not be mkfs.xfs'd by the NVMe provisioner. WAL/segment XFS partitions belong on a separate slice of each disk (set via xfsPartitionPercent) or on the boot disk.

Example CustomResource

Adapted from the operator's shipped production sample (config/samples/veltrix_v1alpha1_veltrixcluster.yaml) — 3 nodes, 480 GiB RAM, 8 NVMe disks each, replication factor 3, raw block-device VLog with a 20/80 XFS/raw partition split:

apiVersion: veltrix.io/v1alpha1
kind: VeltrixCluster
metadata:
  name: veltrixdb-prod
  namespace: veltrixdb
spec:
  # Cluster topology
  size: 3
  version: "1.0.0"
  image: "ghcr.io/veltrixdb/veltrixdb"
  replicationFactor: 3

  # Cache — 256 GiB LIRS cache per node (80% rule for 480 GiB nodes)
  cacheMaxSizeMB: 262144

  # Storage
  storageClassName: local-nvme
  disksPerNode: 8
  diskSize: "375Gi"

  # Resources (n2-highmem-96 class)
  resources:
    requests:
      memory: "480Gi"
      cpu: "60"
    limits:
      memory: "480Gi"
      cpu: "60"

  # Network ports
  serverPort: 9000
  metricsPort: 2112
  gossipPort: 7946

  # Raw NVMe block-device VLog: 20% XFS (keys+WAL), 80% raw (values)
  rawVLog:
    enabled: true
    xfsPartitionPercent: 20

  # Scheduling — one pod per physical host
  podAntiAffinityTopologyKey: "kubernetes.io/hostname"
  nodeSelector:
    cloud.google.com/gke-local-nvme-ssd: "true"
  tolerations:
    - key: workload
      value: veltrixdb
      effect: NoSchedule
kubectl apply -f veltrixcluster.yaml
kubectl get veltrixcluster -n veltrixdb

Status fields

FieldValuesMeaning
phaseProvisioning / Running / Scaling / Degraded / TerminatingOverall cluster lifecycle state, computed from ready-pod count each reconcile.
readyReplicasintegerPods currently passing the readiness probe.
currentVersionstringImage tag from spec.version at last reconcile.
partitionMapstringSummary of the current shard-to-node assignment; updated after a resharding event.
conditionsReady, ReplicationFactorMetStandard Kubernetes conditions. ReplicationFactorMet=False carries the message "DATA LOSS RISK" when ready pods drop below replicationFactor.
lastScaleTimetimestampSet when a resharding event is triggered.
kubectl describe veltrixcluster my-cluster -n veltrixdb

What the reconcile loop actually does

This section is scoped to what is verifiably implemented in controllers/veltrixcluster_controller.go, not just described in prose elsewhere.

Child resource management

Each reconcile pass creates or updates, in order: the local-nvme StorageClass, an NVMe-provisioner DaemonSet (plus its ServiceAccount/ClusterRole/ClusterRoleBinding), the pod ServiceAccount, headless and client Service objects, a best-effort ServiceMonitor (a reconcile failure here only logs a warning — it does not fail the whole reconcile, since the Prometheus Operator CRDs may not be installed), the ConfigMap of engine env vars, and finally the StatefulSet.

Rolling upgrades — confirmed

The StatefulSet is built with updateStrategy.type: RollingUpdate and rollingUpdate.maxUnavailable: 1. Bumping spec.version (or spec.image) and re-applying the CR causes Kubernetes' native StatefulSet controller to replace one pod at a time, waiting for each to become ready before proceeding — the operator does not implement custom upgrade orchestration beyond setting this strategy.

kubectl patch veltrixcluster my-cluster -n veltrixdb \
  --type merge --patch '{"spec":{"version":"1.1.0"}}'

Auto-reshard on replica count changes — confirmed, with caveats

The reconciler compares the StatefulSet's previous replica count to spec.size on every pass. When the cluster has grown (prevSize < spec.size), it calls triggerResharding, which:

  1. Blocks (up to 5 minutes) until all desired pods report PodReady.
  2. Annotates every pod with veltrix.io/cluster-size and veltrix.io/prev-cluster-size. Server fact: nothing in the VeltrixDB server reads these annotations. A server builds its membership once at startup from --peers (or --seeds), gossip does not add members, and there is no runtime join command; on v1.14 and earlier --auto-rebalance migrates keys only when that static membership changes state (a node fails or recovers). Fixed in v1.15 state changes never migrate and raft mode never migrates, so with static membership no keys move. In practice, on v1.16 and earlier a new node joins the ring only when every pod restarts with it listed in --peers. New on main after v1.16 — ships in the next release the server can add and remove members online (/admin/cluster/members, --join, --advertise-addr), but the operator does not use it yet: it still scales by restarting every pod with a new --peers, which stops working once a change was made online (persisted membership wins over --peers). The server repo’s intended pattern: keep --peers at the bootstrap members, add --join=<bootstrap pod addresses> and --advertise-addr=$(POD_FQDN):<port> to every pod, scale up by raising the replica count, and scale down by calling DELETE /admin/cluster/members/<pod> for the highest ordinal before lowering the count. See the membership runbook.
  3. Records status.lastScaleTime and a status.partitionMap summary string.
Scale-down is not specially handled The resharding trigger only fires when the new size is greater than the previous size. Scaling down updates the StatefulSet's replica count (removing the highest-ordinal pods) but does not go through the same annotate-and-wait resharding path.

Self-healing — partially confirmed

The operator itself does not run a custom pod-replacement loop; it relies on the standard StatefulSet controller to recreate any pod that is deleted or fails its liveness probe, which is the same "self-healing" every StatefulSet gets from Kubernetes. What the operator adds on top:

Scope of "self-healing" Pod replacement is delegated to the built-in StatefulSet controller, not a bespoke operator routine. Data rebuild after a replacement is a function of the VeltrixDB binary's own replication/recovery path (WAL replay, replica resync), not code inside the operator's reconcile loop.

Graceful WAL flush on termination — confirmed

Every pod gets a preStop lifecycle hook (kill -SIGTERM 1 && sleep 55 — note the official image is distroless with no shell, so an exec hook of this form cannot run there; the kubelet's own SIGTERM still triggers the server's clean shutdown) and a 60-second terminationGracePeriodSeconds, giving the engine time to flush its WAL before the container is killed. On VeltrixCluster deletion, the operator's finalizer (veltrix.io/finalizer) annotates pods as draining before removing itself — the actual flush is still performed by the pod's own SIGTERM handling and the grace period, not a separate operator-issued flush command.

Scaling

kubectl patch veltrixcluster my-cluster -n veltrixdb \
  --type merge --patch '{"spec":{"size":5}}'

Increasing spec.size triggers the StatefulSet to add pods and, once they are ready, the annotation step described above. Whether the existing pods then learn about the new ones depends on the --peers list your Operator version renders and on a restart; the server does not discover peers at runtime.

Node, seccomp and image notes

Troubleshooting

SymptomCause / fix
Pod crashes with write /mnt/nvmeN/wal.log: no space left on deviceThe pod started before the mount-keeper bound real XFS at the PV source path, so kubelet bind-mounted a 256 KB tmpfs placeholder. Confirm the keeper DaemonSet is Running, then delete the pod so kubelet re-creates the bind mount.
Startup log [transfer] WARNING: listener … is unauthenticatedNo cluster secret and no cluster mTLS: set VELTRIXDB_CLUSTER_SECRET (same value, ≥ 16 bytes, on every pod).
Cluster stuck in ProvisioningCheck kubectl get pv; on GKE confirm the node pool used --local-ssd-interface=NVME; check operator logs via kubectl logs -n veltrixdb-system deploy/veltrixdb-controller-manager.
Resharding not triggering after adding nodesThe operator only watches spec.size changes on reconcile. Force one with kubectl annotate veltrixcluster my-cluster force-reconcile=$(date +%s).
WAL flush timeout on pod terminationIf pods terminate faster than the 30–60s grace window, raise terminationGracePeriodSeconds in the StatefulSet spec.
Cluster Autoscaler deletes database nodes (local NVMe data lost permanently)The operator adds cluster-autoscaler.kubernetes.io/safe-to-evict: "false" automatically; if pods were created by an older operator version, annotate them manually.