
Distributed Storage on Bare Metal
TL;DR
Distributed storage on bare-metal Kubernetes is a four-job problem — synchronous replication, snapshots, off-cluster backup, RWX — and the six contenders in 2026 (Longhorn, Rook-Ceph, OpenEBS Mayastor, Piraeus/LINSTOR, Portworx, local-path-provisioner) each treat one job as primary and the rest as supported-eventually.
Frank runs Longhorn with three replicas across the three
control-plane nodes. The scars came in the seams between Longhorn and
the rest of the declarative stack: a RWO RollingUpdate deadlock, an
empty-ExternalSecret rejection, an ArgoCD-versus-secret-store fight
settled by ServerSideApply=true plus ignoreDifferences on Secret
data.
Frank’s answer does not generalize. One node → local-path. Two-to-four nodes → Longhorn (or Rook-Ceph if RWX is load-bearing). Five-plus → Rook-Ceph is the sweet spot. Production SLA on top of any leaf → Portworx or managed cloud storage.
§1 — The capability
Three nodes go in. Three replicas come out. Then one node is rebooted
for a kernel update, and the only thing that matters is whether the
PVC mounted at /var/lib/postgresql/data still answers a write.
That is the capability under examination. Not “storage” in the abstract — Kubernetes already has the storage abstraction; it is called a PVC and it is provider-agnostic on purpose. The capability is the thing on the other end of CSI on a bare-metal cluster: who keeps your bytes alive when a host you control disappears, and who pays the tax for keeping them alive?
flowchart LR
W["Workloads (PVCs)"] --> C["CSI driver"]
C --> S["Distributed storage layer"]
S --> R["Replication"]
S --> SN["Snapshots"]
S --> B["Off-cluster backup"]
S --> RW["RWX access"]
S -.-> K["Local disks (bare metal)"]
The diagram is honest about what “distributed storage” actually contains. It is not one job; it is at least four — synchronous replication, point-in-time snapshots, off-cluster backup, and read-write-many access for the workloads that need it. Every vendor in this space treats one of those four as the primary problem and relegates the rest to “supported, eventually.” The vendor space splits on which job is primary.
I run Longhorn. That choice was not made on the merits in the abstract; it was made on the merits of the three control-plane nodes I already had. Other clusters have other geometries, and a different shape produces a different answer. The point of this paper is to make the trade legible — capability by capability, with the same diagram language for each vendor — and then return to Frank’s choice and the operational scars that proved it was correct only on Frank’s terms.
§2 — The landscape
Six options dominate distributed storage on bare-metal Kubernetes in 2026, and they split cleanly on two axes. The horizontal axis is licensing — open source on the left, commercial-with-contract on the right. The vertical axis is harder to name without picking a fight, so the diagram calls it centralized versus replicated-per-volume: does the system maintain one cluster-wide data plane (CRUSH map, metadata service, OSD pool), or does it stand up an independent replica set per PVC?
quadrantChart
title Distributed storage on bare metal — 2026
x-axis OSS --> Commercial
y-axis Centralized --> "Replicated per volume"
quadrant-1 "Per-volume · Commercial"
quadrant-2 "Per-volume · OSS"
quadrant-3 "Centralized · OSS"
quadrant-4 "Centralized · Commercial"
"Longhorn": [0.25, 0.75]
"Rook-Ceph": [0.25, 0.25]
"OpenEBS Mayastor": [0.30, 0.80]
"Piraeus / LINSTOR": [0.20, 0.85]
"Portworx": [0.75, 0.70]
"local-path-provisioner": [0.10, 0.95]
| Feature | Longhorn | Rook-Ceph | OpenEBS Mayastor | Piraeus / LINSTOR | Portworx | local-path-provisioner |
|---|---|---|---|---|---|---|
| Block (RWO) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| File (RWX) | 🟡 | ✅ | ❌ | ❌ | ✅ | ❌ |
| Object (S3) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Snapshots | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| Off-cluster backup | ✅ | ✅ | 🟡 | 🟡 | ✅ | ❌ |
| Synchronous replication | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| OSS / no contract required | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Min healthy nodes | 3 | 5 | 3 | 2 | 3 | 1 |
The matrix grades the options on the four jobs from §1 plus the license axis and a minimum-node count. The min-node column is the one that does the most work; it is also the one most vendor docs mention only in a footnote.
Longhorn optimises for operator legibility. Rancher’s UI shows you every volume, every replica, every snapshot — and the per-volume engine model means a corrupted Postgres volume cannot poison the storage of an unrelated workload. The trade is that the same per- volume architecture is genuinely worse than CRUSH for huge fleets. Longhorn is comfortable at ≤10 nodes and stops scaling well around 50.
Rook-Ceph is the inverse trade. You install an operator and you get Ceph — a real distributed filesystem with twenty years of research behind it, unified block/file/object, the works. The price is admission: Ceph wants ≥5 hosts to populate a healthy CRUSH map under the default rule, and you must learn enough Ceph to debug when the operator’s friendly abstraction leaks.
Ceph maximizes the separation between data and metadata management by replacing allocation tables with a pseudo-random data distribution function (CRUSH) designed for heterogeneous and dynamic clusters of unreliable object storage devices (OSDs).Weil et al., Ceph (OSDI '06)
OpenEBS Mayastor is the performance answer. NVMe-over-Fabrics on the data path; the control plane is a thin Kubernetes operator. Mayastor was the first project in this list to treat the NVMe latency floor as the primary design constraint, and the design has aged well — newer projects in this space converge toward its shape.
Piraeus / LINSTOR is DRBD with a Kubernetes wrapper. DRBD has been in the Linux kernel since 2009; LINBIT has been selling support for it since before Kubernetes existed. The unique trade is that synchronous replication runs inside the kernel rather than in userspace, which means the latency floor is very low and the operational model is very unlike everything else on this list. Two-node clusters are supported, which no other option here can honestly claim.
Portworx is the commercial answer. Per-volume replication, RWX, PX-Backup, snapshots, an SLA, a contract, a phone number. The other five options on this list ask you to operate them; Portworx asks you to pay them.
local-path-provisioner is the null hypothesis. It provisions hostPath PVCs. There is no replication, no snapshot, no backup, no failover. Its purpose in this paper is to mark the lower bound: if your cluster has one node, this is the right answer, and the rest of the matrix is solving a problem you do not have.
§3 — How each option handles the hard part
The hard part of distributed storage is surviving a node loss without losing acknowledged writes. Every vendor on this list has an answer; the answers diverge enough that they need separate diagrams. The diagrams below use a shared visual language — squares for control-plane components, cylinders for data on disk, hexagons for clients (CSI driver, kernel module), solid edges for read/write hot paths, dashed edges for replication and failover.
Longhorn
flowchart TD
subgraph LH["Longhorn (per-volume)"]
M["longhorn-manager"]
E1[("Engine pod · vol-A")]
R1[("Replica · mini-1")]
R2[("Replica · mini-2")]
R3[("Replica · mini-3")]
end
C{{"CSI driver"}}
C --> E1
E1 --> R1
E1 -.-> R2
E1 -.-> R3
M --> E1
Each PVC gets a dedicated engine pod and three replica pods, each replica on a different node. The engine writes to the local replica synchronously and forwards to the other two over the network. When the node hosting the engine dies, the manager elects a new engine on a surviving replica’s node and the CSI driver re-attaches the volume. Time-to-recovery is dominated by the kubelet’s pod-eviction grace period, not by storage machinery — typically 30–90 seconds before a new engine pod is scheduled and the volume is re-mounted on the workload’s pod.
The failure mode is per-volume, which is both a feature and a
limit. A corrupt replica on mini-1 taints vol-A only;
vol-B’s replicas are independent. The limit: at 200 volumes
the cluster runs 200 engine pods, each holding its own TCP
connections to its replicas. Longhorn is not designed for that
density.
Rook-Ceph
flowchart TD
subgraph RC["Rook-Ceph (centralized)"]
OP["rook-operator"]
MGR["ceph-mgr"]
MON["ceph-mon (×3)"]
OSD1[("OSD · node-1")]
OSD2[("OSD · node-2")]
OSD3[("OSD · node-3")]
OSD4[("OSD · node-4")]
OSD5[("OSD · node-5")]
end
C{{"CSI driver (ceph-csi)"}}
C --> MON
C --> OSD1
OSD1 -.-> OSD2
OSD1 -.-> OSD3
OP --> MGR
MGR --> OSD1
MGR --> OSD2
The data plane is one Ceph cluster. OSDs run as DaemonSets;
monitors hold the cluster map; CRUSH decides where objects live.
A workload’s PVC is a block image carved out of a Ceph pool, and
the ceph-csi driver maps that image into the workload’s pod via
the kernel’s rbd module. When a node dies, CRUSH recomputes
placement and the surviving OSDs accept the orphaned PG replicas
within seconds. The workload’s I/O blocks on the kernel-level
client briefly, then resumes.
The architecture is genuinely better than Longhorn’s per managed megabyte, and the price is the five-node minimum for a healthy default CRUSH rule plus enough Ceph fluency to debug when something below the operator abstraction fails.
OpenEBS Mayastor
flowchart TD
subgraph MS["Mayastor (NVMe-oF)"]
AGT["mayastor-agent (×N)"]
N1[("Nexus · node-1\n(initiator)")]
T1[("NVMe target · node-2")]
T2[("NVMe target · node-3")]
end
C{{"CSI driver (NVMe-oF)"}}
C --> N1
N1 --> T1
N1 -.-> T2
AGT --> N1
Mayastor builds a Nexus (an NVMe-oF initiator) on the node running the workload, which front-ends one or more NVMe targets on other nodes. Writes are synchronously fanned out to all targets. The data path is NVMe-oF over TCP (or RDMA where the fabric supports it), which means the latency floor is set by the NIC, not by a userspace storage engine.
Failure recovery rebuilds the Nexus on a surviving node and re-mounts the volume. The bet of the architecture is that NVMe-oF will dominate the next decade of storage hardware, and that betting on the protocol early is the right call. So far the bet looks correct.
Piraeus / LINSTOR
flowchart TD
subgraph PR["Piraeus / LINSTOR (DRBD)"]
CTRL["LINSTOR controller"]
S1[("DRBD primary · node-1\n(kernel)")]
S2[("DRBD secondary · node-2")]
end
C{{"CSI driver"}}
C --> S1
S1 -.-> S2
CTRL --> S1
CTRL --> S2
DRBD has been in the Linux kernel since 2009. Piraeus puts a Kubernetes operator (LINSTOR) in front of it and exposes the resulting block devices via a CSI driver. The data path runs inside the kernel, which means there is no userspace engine to crash and the failover model is the same as a thirty-year-old high-availability cluster: a primary and one or more secondaries; on primary loss, a secondary is promoted.
The architecture is the only one on this list that survives honestly on two nodes — DRBD was designed for two-node high-availability before Kubernetes existed. The trade is that the operational model is unlike everything else in cloud-native storage. You are not really operating a Kubernetes storage system; you are operating DRBD, with a Kubernetes wrapper.
Portworx
flowchart TD
subgraph PX["Portworx (commercial)"]
PXC["px-central (SaaS)"]
PX1[("Storage node · node-1")]
PX2[("Storage node · node-2")]
PX3[("Storage node · node-3")]
end
C{{"CSI driver"}}
C --> PX1
PX1 -.-> PX2
PX1 -.-> PX3
PXC --> PX1
Architecturally similar to Longhorn — per-volume replicas across named storage nodes — but with a commercial control plane, a managed backup product (PX-Backup), an SLA, and a phone number. Failure recovery is no different in shape; the difference is that when the recovery does not work, somebody else is responsible for figuring out why. The diagram is mostly unremarkable on purpose. The point of Portworx is not its architecture; it is its contract.
§4 — What scale changes
Three scale axes flip vendor rankings. The first two are quantitative; the third is philosophical.
Node count. Ceph’s CRUSH map wants at least five OSD hosts to
produce a healthy default CRUSH rule with the standard host
failure domain and three replicas — the math comes straight from
the OSDI ‘06 paper and has not changed since.
We have designed and implemented Ceph, a distributed file system that provides excellent performance, reliability, and scalability.Weil et al., Ceph (OSDI '06)
Longhorn’s per-volume replicas work cleanly at three. Piraeus / LINSTOR survives on two. local-path-provisioner is single-node by construction. The decision tree in §6 makes this concrete: node count is the first branch, and it eliminates more vendors than any other criterion.
The community consensus on the Ceph minimum is not folklore — it shows up in every practitioner thread on the topic:
Longhorn is much simpler to set up and operate. Ceph is more powerful but requires more nodes and more tuning to perform well. I run Longhorn on a 3-node cluster with NVMe drives and it just works.r/kubernetes — Longhorn vs Ceph thread
Replica count versus write throughput. Synchronous replication
taxes write throughput by approximately N× where N is the replica
count and the network is the bottleneck. On NVMe drives behind a
1 GbE NIC, the network always is. The interesting consequence: if
your workload is write-heavy and your fabric is unimpressive,
moving from replicaCount: 3 to replicaCount: 1 is not a small
optimisation — it is a 3× write-IOPS improvement, paid for with the
durability the cluster was supposedly there to provide. Frank’s
default of three replicas is honest only because Frank has three
control-plane nodes; the math of three-way replication is durable
only on three failure domains.
The NVMe latency floor. Mayastor’s NVMe-oF design aims at the hardware floor — typically tens of microseconds on a quiet link. Longhorn’s userspace engine accepts a higher floor in exchange for operational simplicity; the engine pod handles snapshots, replica rebuilds, and protocol bookkeeping at the cost of an extra context-switch on every I/O. This is the philosophical split that defines the space: performance-first vendors trade debuggability for latency, operability-first vendors trade latency for legibility. There is no objectively correct answer, but the answer is heavily constrained by what kind of operator is on the keyboard at 2 AM.
§5 — Frank’s choice, and what happened
I run Longhorn. Three replicas across the three control-plane
nodes — mini-1, mini-2, mini-3. Not because the math of
replica_count == control_plane_count is durable (it isn’t,
mathematically: losing two of three control-plane nodes loses
both quorum and two of three replicas in one event), but
because that was the geometry of the fleet I already had, and I
was not going to buy a fourth NUC to satisfy a math problem.
The honesty of that choice is what makes the resulting scars worth writing down. A managed cluster would have hidden every one of them.
Progressing indefinitely. Switching
the Helm chart’s strategy to Recreate fixed it on paper, and
then refused to fix it on the cluster — Helm’s chart rendering
cannot delete keys from a live resource, and the old
rollingUpdate: block survived the strategy change as an orphan
field that re-enabled the broken behavior. The fix was a one-time
kubectl patch to delete the orphan block, after which the chart
rendered cleanly forever. An immutability boundary inside an
otherwise declarative stack: declarative tooling does not infer
“absent” from “removed from the values file.”data: [] was rejected by the admission
webhook. There is no “empty secret” valid state — when all keys
are removed, the ExternalSecret itself must be deleted, not
zeroed. ArgoCD will not infer that from a values diff. A human
has to know, which is to say: the operator has to know, which is
to say: the gotcha has to be written down or the next migration
re-discovers it.ServerSideApply=true, prune: false, and ignoreDifferences on
Secret /data aren’t decorations on the Application CR — they’re
the only thing that prevents ArgoCD from fighting the secret store
at every sync. Learning that the first time costs an afternoon;
encoding it in every Application CR forever costs nothing. The
shape of the lesson is the same as the lesson itself: declarative
infrastructure needs an explicit policy for the resources it does
not own, or it pretends to own everything and breaks the things
it cannot.The three scars share a shape. None of them are bugs in Longhorn; all of them are emergent properties of running a storage layer that the cluster’s other declarative machinery does not entirely understand. The interfaces between Longhorn, ArgoCD, External Secrets, and the admission controllers are where the failures live — exactly where the marketing material does not look.
Visible evidence:

A managed-storage product would have hidden every one of these failure modes behind its SLA, which is the right trade for a production team and the wrong trade for a learning platform. Frank exists to encounter the RWO-RollingUpdate deadlock so that the next operator on this stack does not have to.
§6 — When Frank’s answer doesn’t generalize
Frank’s answer — Longhorn on three control-plane nodes — is one leaf of a four-leaf tree. The other three are real.
flowchart TD
A["How many nodes do you have?"] -- "1" --> L1["local-path-provisioner"]
A -- "2–4" --> B["RWX heavy?"]
B -- "No" --> L2["Longhorn"]
B -- "Yes" --> L3["Rook-Ceph (accept the operational tax)"]
A -- "≥5" --> L4["Rook-Ceph (the sweet spot)"]
L1 -.-> P["Production SLA needed?"]
L2 -.-> P
L3 -.-> P
L4 -.-> P
P -- "Yes" --> PX["Portworx or managed cloud storage"]
The first branch is node count. A single node has no distributed
storage problem; local-path-provisioner is correct, full stop.
At two-to-four nodes the question becomes whether RWX matters:
Longhorn does RWX through NFS-on-top, which is workable for log
collectors and broken for write-heavy multi-writer workloads;
Rook-Ceph’s CephFS does RWX natively but extracts the five-node
operational tax even when the cluster is small. At five-plus
nodes Rook-Ceph stops being a hard sell and becomes the sweet
spot — the operational tax amortises, and you get unified
block/file/object out of one operator.
The dashed branch is the SLA override. Any of the four leaves can be promoted to “Portworx or managed cloud storage” when the business needs a contract behind the data plane. Frank doesn’t — the cluster’s contract is a personal commitment to write down what breaks. A real team with a real on-call rotation and real revenue depending on uptime probably should.
This is the section where the paper has to be honest about its audience. If you are reading this from a production engineering team, the right answer for you is almost never Frank’s answer. The right answer is the SLA branch. Frank’s answer is correct for Frank and is documented here so that anyone considering the trade understands the rest of the leaves before picking the same one.
§7 — Roadmap & where this space is going
Three trends are worth naming. None of them are settled; all of them affect the next few years of distributed storage on bare metal.
NVMe-over-Fabrics is going mainstream. Mayastor was early — the design is becoming the expected baseline for any new distributed-storage project, and the latency floor moves with the hardware. Once NVMe-oF over RDMA is the assumed substrate, the userspace-engine model that powers Longhorn looks like a legacy choice rather than an operability win. The interesting question is not whether NVMe-oF wins, but whether the operability-first projects (Longhorn especially) can adopt it without losing the per-volume isolation that is their actual selling point.
Rook-Ceph’s operator maturity is closing the gap. The biggest historical argument against Rook-Ceph was the operational tax — you needed enough Ceph fluency to debug under the abstraction. The Rook operator’s recent releases have closed a lot of that gap, with much friendlier failure modes and far less manual intervention required for routine OSD failures. The “Rook tax” is shrinking. The five-node floor is not.
Snapshot and backup standardization. The CSI snapshot API, Velero, and Kopia layered on top are reaching the point where “which backup tool” is no longer a vendor-lock-in decision. Off-cluster backup is becoming portable across storage backends — the same Kopia repository can hold snapshots from Longhorn, Rook-Ceph, and Mayastor without surgery. This matters less for single-vendor clusters like Frank and matters enormously for anyone planning to migrate between storage backends without re-snapshotting their entire data estate.
The space is not done evolving. Frank will revisit this paper when the answers change.
References
- vendor-docs
Longhorn — Architecture and Concepts
Longhorn creates a dedicated storage controller for each volume and synchronously replicates the volume across multiple replicas stored on multiple nodes.
The default replica count is 3. Each replica is placed on a different node based on the configured replica scheduling policy.
Vendor's own articulation of Longhorn's per-volume-engine architecture. Defines the model Frank actually runs (engine pod + N replica pods per volume) and grounds the §3 architecture comparison in Longhorn's authoritative description.
- vendor-docs
Rook — Architecture overview
Rook is an open source cloud-native storage orchestrator, providing the platform, framework, and support for Ceph storage to natively integrate with cloud-native environments.
Rook automates deployment, bootstrapping, configuration, provisioning, scaling, upgrading, migration, disaster recovery, monitoring, and resource management.
Definitive description of the operator-driven Ceph deployment model. Anchors the §3 architecture diagram for Rook-Ceph and the §4 'CRUSH wants ≥5 OSD hosts' rule.
- paper
Ceph: A Scalable, High-Performance Distributed File System (Weil et al., OSDI '06)
We have designed and implemented Ceph, a distributed file system that provides excellent performance, reliability, and scalability.
Ceph maximizes the separation between data and metadata management by replacing allocation tables with a pseudo-random data distribution function (CRUSH) designed for heterogeneous and dynamic clusters of unreliable object storage devices (OSDs).
Foundational academic paper that introduces the CRUSH placement algorithm, the same machinery Rook still deploys twenty years later. Used in §2 and §3 to anchor the centralized-storage definition and explain why Ceph's minimum-host count is what it is.
- benchmark
Longhorn vs Ceph (Rook) — Reddit r/kubernetes practitioner thread
Longhorn is much simpler to set up and operate. Ceph is more powerful but requires more nodes and more tuning to perform well.
I run Longhorn on a 3-node cluster with NVMe drives and it just works.
Practitioner-level head-to-head with reproducible setup notes — explicitly names the 'Ceph needs more nodes to perform' rule of thumb cited in §4. Not a controlled benchmark, but the closest thing the community has to one at homelab scale, and representative of the consensus.
- postmortem
Frank — Storage / Secrets / SSA gotchas (RWO PVC + RollingUpdate deadlock, ESO empty-data rejection)
RWO PVC + RollingUpdate deadlocks; use strategy: Recreate. Switching strategy via Helm needs a one-time kubectl patch to clear the orphan rollingUpdate: block.
ESO: empty data: [] is rejected; delete the ExternalSecret if all keys are removed.
SOPS-encrypted secrets must NOT be ArgoCD-managed; apply out-of-band from secrets/.
Frank's own running postmortem registry — concrete operational scars accumulated while running Longhorn in production for this learning platform. Provides the source-of-truth dates and recovery commands for §5 scar callouts and underwrites the §6 decision-tree branches.
