{"id":122,"date":"2026-04-10T11:27:28","date_gmt":"2026-04-10T16:27:28","guid":{"rendered":"https:\/\/blog.lfps64.com\/?p=122"},"modified":"2026-04-10T13:32:08","modified_gmt":"2026-04-10T18:32:08","slug":"building-a-proxmox-home-cluster-without-shared-storage-ha-or-quorum-worries","status":"publish","type":"post","link":"https:\/\/blog.lfps64.com\/?p=122","title":{"rendered":"Building a Proxmox Home Cluster Without Shared Storage, HA, or Quorum Worries"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\"><\/h1>\n\n\n\n<p><em>A practical guide to clustering heterogeneous homelab nodes the right way<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>If you&#8217;ve been running multiple Proxmox nodes as independent standalone hosts and decided to bring them together into a single cluster, you probably hit the same wall I did: most of the clustering documentation assumes you have enterprise-grade shared storage, fencing devices, and a dedicated cluster network. In a homelab, you have none of that \u2014 and if you proceed naively, you end up with a cluster that&#8217;s more fragile than your original standalone setup.<\/p>\n\n\n\n<p>This post documents the challenges I faced when clustering three heterogeneous nodes and how I solved each one.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Setup<\/h2>\n\n\n\n<p>Three nodes, all running Proxmox VE 9, each with its own local storage:<\/p>\n\n\n\n<ul>\n<li>A lightweight mini PC running 24\/7, designated as cluster master<\/li>\n\n\n\n<li>A heavier compute node with more RAM and storage, used for demanding workloads<\/li>\n\n\n\n<li>A third mini PC running a specific NVR workload<\/li>\n<\/ul>\n\n\n\n<p>None of the nodes share a storage pool. Each has its own NVMe SSDs, HDDs, and LVM-thin pools. No SAN, no Ceph, no NFS for VM disk storage \u2014 just local disks.<\/p>\n\n\n\n<p>The goal was simple: a single Proxmox UI to manage everything, with each node remaining fully independent and capable of booting its VMs regardless of whether the other nodes were reachable.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Challenge 1: Hostname and DNS Consistency<\/h2>\n\n\n\n<p>Before even thinking about <code>pvecm create<\/code>, Proxmox requires that all nodes can resolve each other by FQDN. Corosync and the cluster filesystem (<code>pmxcfs<\/code>) depend on it.<\/p>\n\n\n\n<p>In my case, two nodes had their search domain set to <code>local.homelab.net<\/code> while the third was still on the old <code>homelab.net<\/code>. The result: <code>hostname --fqdn<\/code> returned different domains across nodes, which would cause cluster communication issues down the line.<\/p>\n\n\n\n<p><strong>The fix:<\/strong> Standardize all nodes to the same search domain before touching anything cluster-related. On Proxmox, don&#8217;t edit <code>\/etc\/resolv.conf<\/code> directly \u2014 it can be overwritten. Use the PVE API instead:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>pvesh set \/nodes\/&lt;nodename&gt;\/dns --search local.homelab.net\n<\/code><\/pre>\n\n\n\n<p>Verify with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>hostname --fqdn\n<\/code><\/pre>\n\n\n\n<p>All three nodes should return <code>&lt;nodename&gt;.local.homelab.net<\/code> before proceeding.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Challenge 2: The Quorum Problem for Standalone Nodes<\/h2>\n\n\n\n<p>This is the big one, and it catches most homelab admins off guard.<\/p>\n\n\n\n<p>Proxmox clusters use <strong>Corosync<\/strong> for node heartbeating and <strong>quorum<\/strong> to determine cluster health. The default behavior: if a node loses quorum (can&#8217;t reach enough peers), Proxmox <strong>freezes VM operations on that node<\/strong>. It won&#8217;t start VMs, it won&#8217;t stop them gracefully \u2014 everything just hangs.<\/p>\n\n\n\n<p>Quorum fencing exists for a good reason in enterprise environments: if two nodes can both write to the same shared disk simultaneously, you get catastrophic data corruption. Quorum prevents this by shutting down the &#8220;minority&#8221; side.<\/p>\n\n\n\n<p>But here&#8217;s the thing \u2014 if you have no shared storage, there is no shared disk to corrupt. Each node only touches its own local disks. Quorum fencing in this scenario provides zero protection and causes real pain: if your cluster master goes offline for maintenance, your other two nodes refuse to start VMs until it comes back.<\/p>\n\n\n\n<p><strong>The fix:<\/strong> Set <code>no_quorum_policy: ignore<\/code> in Corosync configuration. This tells the cluster to keep running VM operations even when quorum is lost.<\/p>\n\n\n\n<p>After creating the cluster, edit <code>\/etc\/pve\/corosync.conf<\/code> and add it to the quorum section:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>quorum {\n  provider: corosync_votequorum\n  expected_votes: 3\n  no_quorum_policy: ignore\n}\n<\/code><\/pre>\n\n\n\n<p>With this in place, each node operates independently even if its peers are unreachable. You get the unified management UI when everything is up, and you get resilience when things are down.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Challenge 3: Ghost Disks \u2014 The Silent Disaster<\/h2>\n\n\n\n<p>When nodes join a cluster, Proxmox replicates <code>storage.cfg<\/code> cluster-wide. This means every node suddenly &#8220;sees&#8221; the storage pools defined on all other nodes \u2014 including local LVM-thin pools that physically only exist on one machine.<\/p>\n\n\n\n<p>The result: a node will happily display another node&#8217;s local storage as &#8220;available,&#8221; and if you accidentally provision a VM disk there, it will fail silently or corrupt. Even worse, Proxmox&#8217;s UI won&#8217;t clearly warn you that you&#8217;re trying to use storage that doesn&#8217;t exist locally.<\/p>\n\n\n\n<p>This is the ghost disk problem.<\/p>\n\n\n\n<p><strong>The fix:<\/strong> Use the <code>nodes=<\/code> directive in <code>storage.cfg<\/code> to restrict each storage pool to only the node it physically lives on.<\/p>\n\n\n\n<p>Example <code>storage.cfg<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>lvmthin: fast-nvme\n    thinpool fast-nvme\n    vgname fast-nvme\n    content rootdir,images\n    nodes node1\n\nlvmthin: secondary-ssd\n    thinpool secondary-ssd\n    vgname secondary-ssd\n    content rootdir,images\n    nodes node2\n\nlvmthin: nvr-storage\n    thinpool nvr-storage\n    vgname nvr-storage\n    content rootdir,images\n    nodes node3\n<\/code><\/pre>\n\n\n\n<p>The <code>nodes=<\/code> line means that storage only appears in the UI when you&#8217;re looking at the correct node. No cross-contamination, no ghost disks.<\/p>\n\n\n\n<p><strong>Important:<\/strong> <code>storage.cfg<\/code> is cluster-wide and lives in <code>\/etc\/pve\/<\/code>. Any node can modify it, but changes replicate everywhere. Edit it once after all nodes have joined \u2014 not before, because a node join overwrites the local <code>storage.cfg<\/code> with the master&#8217;s copy.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Challenge 4: VMs Vanish From the UI After Joining<\/h2>\n\n\n\n<p>Storage was fixed, the cluster was up, all three nodes were showing green. Then I noticed something alarming: the VMs and containers on two of the three nodes had completely disappeared from the Proxmox web UI \u2014 not stopped, not errored, just <em>gone<\/em>. No entries at all under those nodes.<\/p>\n\n\n\n<p>To be clear about what &#8220;disappeared&#8221; means here: the VMs were still running. Every service was reachable, every SSH session connected, every workload humming along normally. The problem was purely at the management layer \u2014 Proxmox itself had lost track that those VMs existed. You couldn&#8217;t start, stop, snapshot, or manage them through the UI or API. They were invisible to the cluster, but alive on the metal.<\/p>\n\n\n\n<p><strong>What happened:<\/strong> When a standalone node joins a cluster, Proxmox transitions its local filesystem to <code>pmxcfs<\/code> \u2014 the distributed cluster filesystem. As part of this transition, VM and container configuration files need to be migrated from the old standalone path into the new cluster-aware path at <code>\/etc\/pve\/nodes\/&lt;nodename&gt;\/qemu-server\/<\/code>. In my case, that migration silently failed on two of the three nodes. The config files weren&#8217;t in the old path, weren&#8217;t in the new path \u2014 they were nowhere on the live filesystem.<\/p>\n\n\n\n<p>Checking both locations confirmed the worst:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ls \/etc\/pve\/nodes\/&lt;nodename&gt;\/qemu-server\/   # empty\nls \/etc\/pve\/qemu-server\/                    # empty\n<\/code><\/pre>\n\n\n\n<p><strong>Where the configs actually were:<\/strong> Before completing the join, Proxmox automatically creates a compressed SQLite backup of the node&#8217;s cluster database at <code>\/var\/lib\/pve-cluster\/backup\/<\/code>. The configs were in there \u2014 they just never made it out into the live filesystem.<\/p>\n\n\n\n<p>The recovery process: extract the backup into a temporary SQLite database, query it for the config files, and write them directly into the correct cluster path.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Load the backup into a temporary database\nzcat \/var\/lib\/pve-cluster\/backup\/config-&lt;timestamp&gt;.sql.gz | sqlite3 \/tmp\/node-restore.db\n\n# Inspect the schema \u2014 it uses 'name', not 'path'\nsqlite3 \/tmp\/node-restore.db \"SELECT name FROM tree WHERE name LIKE '%.conf';\"\n\n# Restore each VM config to its correct cluster path\nfor vmid in 100 102 103 105; do\n  sqlite3 \/tmp\/node-restore.db \\\n    \"SELECT data FROM tree WHERE name='${vmid}.conf';\" \\\n    &gt; \/etc\/pve\/nodes\/&lt;nodename&gt;\/qemu-server\/${vmid}.conf\ndone\n\n# For LXC containers, the path differs\nfor ctid in 300 301; do\n  sqlite3 \/tmp\/node-restore.db \\\n    \"SELECT data FROM tree WHERE name='${ctid}.conf';\" \\\n    &gt; \/etc\/pve\/nodes\/&lt;nodename&gt;\/lxc\/${ctid}.conf\ndone\n<\/code><\/pre>\n\n\n\n<p>After writing the configs, the VMs reappeared in the UI immediately \u2014 no restart required. <code>pmxcfs<\/code> picks up new files in real time.<\/p>\n\n\n\n<p><strong>The lesson:<\/strong> If VMs disappear from the UI after a node joins the cluster, don&#8217;t panic and don&#8217;t touch the running workloads. The data and the disks are fine. The configs are almost certainly in the SQLite backup. Check <code>\/var\/lib\/pve-cluster\/backup\/<\/code> first.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Challenge 5: Why I Deliberately Skipped HA<\/h2>\n\n\n\n<p>Proxmox&#8217;s High Availability feature is prominently visible in the UI, and it&#8217;s tempting to think &#8220;I have a cluster now, I should enable HA on my important VMs.&#8221; Resist this.<\/p>\n\n\n\n<p>HA in Proxmox works by detecting that a node has gone offline and automatically restarting its VMs on a surviving node. This requires two things that a local-storage homelab doesn&#8217;t have: shared storage (so the surviving node can actually access the VM&#8217;s disk) and a fencing mechanism (a way to guarantee the original node is truly dead before another node starts the same VM, preventing two nodes from writing to the same disk simultaneously).<\/p>\n\n\n\n<p>Without both of these, enabling HA causes more problems than it solves. If a node goes offline, the cluster will repeatedly attempt to migrate and restart the VM on another node \u2014 and repeatedly fail, because the disk isn&#8217;t there. The cluster enters a retry loop, the VM ends up in an undefined state, and you&#8217;re left untangling it manually.<\/p>\n\n\n\n<p>The deliberate choice here is to simply not use HA at all. The cluster serves a different purpose in this setup: unified management, a single web UI, and consolidated monitoring. Each node is responsible for its own VMs. If a node goes down, its VMs go down with it \u2014 intentionally and cleanly, with no cluster intervention. That&#8217;s fine for a homelab. You know where your VMs live, you know how to bring them back, and you don&#8217;t need the cluster to second-guess you.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Order of Operations<\/h2>\n\n\n\n<p>Getting the sequence right matters. Here&#8217;s what worked:<\/p>\n\n\n\n<ol>\n<li>Fix hostnames and DNS on all nodes first<\/li>\n\n\n\n<li>Back up each node&#8217;s <code>storage.cfg<\/code> before touching anything<\/li>\n\n\n\n<li>Rename\/fix any storage pools that have naming conflicts between nodes<\/li>\n\n\n\n<li>Create the cluster on the master node (<code>pvecm create<\/code>)<\/li>\n\n\n\n<li>Immediately set <code>no_quorum_policy: ignore<\/code> in <code>corosync.conf<\/code><\/li>\n\n\n\n<li>Join remaining nodes one at a time (<code>pvecm add --force<\/code>)<\/li>\n\n\n\n<li>After all nodes have joined, edit <code>storage.cfg<\/code> once to add <code>nodes=<\/code> restrictions to every local storage pool<\/li>\n\n\n\n<li>Verify in the UI that each node only shows its own storage<\/li>\n\n\n\n<li>If VMs are missing from the UI, recover configs from <code>\/var\/lib\/pve-cluster\/backup\/<\/code> \u2014 don&#8217;t touch the running workloads<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">End Result<\/h2>\n\n\n\n<p>After working through all of this, the outcome is exactly what a homelab cluster should be: a single pane of glass for managing all your nodes, with each one remaining fully autonomous. Any node can go down for maintenance, upgrades, or power savings without affecting the others. The UI shows everything in one place when nodes are reachable, and gracefully marks them offline when they&#8217;re not.<\/p>\n\n\n\n<p>No shared storage required. No HA complexity. No quorum anxiety.<\/p>\n\n\n\n<p>If your homelab nodes are heterogeneous machines with local-only storage, this approach is the right one \u2014 just make sure you address the gotchas before you start, not after.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><em>Running Proxmox VE 9.x across all nodes. Commands and behavior may vary slightly on older versions.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>A practical guide to clustering heterogeneous homelab nodes the right way If you&#8217;ve been running multiple Proxmox nodes as independent standalone hosts and decided to bring them together into a single cluster, you probably hit the same wall I did: most of the clustering documentation assumes you have enterprise-grade shared storage, fencing devices, and a &hellip; <a href=\"https:\/\/blog.lfps64.com\/?p=122\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Building a Proxmox Home Cluster Without Shared Storage, HA, or Quorum Worries&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[25],"tags":[16,22],"_links":{"self":[{"href":"https:\/\/blog.lfps64.com\/index.php?rest_route=\/wp\/v2\/posts\/122"}],"collection":[{"href":"https:\/\/blog.lfps64.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.lfps64.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.lfps64.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.lfps64.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=122"}],"version-history":[{"count":1,"href":"https:\/\/blog.lfps64.com\/index.php?rest_route=\/wp\/v2\/posts\/122\/revisions"}],"predecessor-version":[{"id":123,"href":"https:\/\/blog.lfps64.com\/index.php?rest_route=\/wp\/v2\/posts\/122\/revisions\/123"}],"wp:attachment":[{"href":"https:\/\/blog.lfps64.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=122"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.lfps64.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=122"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.lfps64.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=122"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}