Building a 3-Node Proxmox Cluster on Hetzner with vSwitch Networking A complete guide to deploying a production-ready 3-node Proxmox VE cluster on Hetzner dedicated servers using two Hetzner vSwitches — one for private cluster heartbeat and VM traffic, one for public IP connectivity. Covers Hetzner Robot vSwitch provisioning, bridge configuration with correct MTU for VXLAN encapsulation, Proxmox cluster creation with Corosync bound to the private network, PfSense deployment as the cluster-wide VM gateway, and the per-node MAC address ceiling for VM density planning. Architecture & Prerequisites Diagram Hetzner Robot — vSwitch Setup All Robot, No SSH Everything in this chapter happens in the Hetzner Robot web UI. You're not touching the servers yet — just provisioning the two vSwitches and wiring up the subnet. vSwitches themselves are free. The cost is the additional public subnet you'll order in Step 3. Check current Hetzner pricing before confirming that order. Step 1 — Create the Private vSwitch Log in to Hetzner Robot and go to vSwitch in the left sidebar. Click Order vSwitch . Name it something like priv-cluster . A VLAN ID gets auto-assigned (typically somewhere in the 4000s). You can set a custom one if you have a preference. Write this VLAN ID down — you'll need it in Chapter 3 when configuring the bridges. Don't attach any subnet to this one. It's a private layer-2 only — the nodes will assign their own IPs. Confirm. Step 2 — Create the Public vSwitch Order another vSwitch, name it something like pub-vms . Write down its VLAN ID too. You need both VLAN IDs in the next chapter. Confirm — don't attach anything yet, that's Step 3. Step 3 — Order an Additional Subnet and Route It to the Public vSwitch This is the step that trips people up the first time. The instinct is to order an IP and assign it to a server. Don't do that here. The subnet gets routed to the vSwitch — not to any server, not to a VM. Once it's routed to the vSwitch, every server that's a member of that vSwitch can bridge to those IPs through a VM. In our case that VM is PfSense, which will claim one of those IPs as its WAN address. In Robot, go to IP addresses → Order additional IP addresses . Select Additional subnet . A /29 gives you 6 usable IPs — enough for PfSense and a handful of future public services. Go bigger if you know you'll need more. When asked where to route it: select vSwitch and pick pub-vms . Confirm. The subnet shows up in the vSwitch detail page once Hetzner provisions it — usually a few minutes. Once it's provisioned, Hetzner's network routes that subnet to the vSwitch at their edge. Nothing on any server needs to hold that route. PfSense picks up an IP from it in Chapter 5. Step 4 — Add All Three Servers to Both vSwitches Open priv-cluster , go to Servers , add all three nodes. Do the same for pub-vms . Before Moving On Confirm in Robot: Private vSwitch: 3 servers listed, no subnet, VLAN ID noted Public vSwitch: 3 servers listed, your subnet visible, VLAN ID noted You can't verify anything server-side until the bridges are up in the next chapter. Network Configuration on Each Node Two Bridges, Three Nodes You're adding vmbr_priv and vmbr_pub to each node. The vmbr0 bridge from the first guide stays exactly as it is — these are additions. The config is identical across all three nodes except for the IP on the private bridge, which is unique per node. You'll need the two VLAN IDs from the previous chapter. They appear as and below — substitute your actual values everywhere they appear. MTU 1400 — Why It Matters and Why People Skip It Every unexplained connection problem I've run into on Hetzner vSwitches has come down to MTU. The symptoms are the worst kind — small packets work fine, SSH works fine, a quick curl works fine, but large file transfers stall or a VPN never fully handshakes. It looks like a routing problem or a firewall rule. It isn't. Hetzner vSwitches use VXLAN internally. VXLAN adds 50 bytes of header overhead per frame. If your bridges and VM interfaces sit at the standard 1500-byte MTU, frames near that ceiling get silently fragmented. Set everything to 1400 and that whole class of problem disappears. This applies to the bridges here, PfSense's interfaces in Chapter 5, and every VM NIC in Chapter 6 — anything that touches a vSwitch bridge. Step 1 — Enable VLAN Support on Each Node apt-get install -y vlan echo "8021q" >> /etc/modules modprobe 8021q Step 2 — Edit /etc/network/interfaces Append the blocks below to the existing file on each node. If your main NIC is eth0 rather than eno1 , swap the name throughout ( ip link show if you're unsure). Node 1 — pve1 # Private vSwitch auto eno1. iface eno1. inet manual vlan-raw-device eno1 auto vmbr_priv iface vmbr_priv inet static address 10.100.101.1/23 bridge-ports eno1. bridge-stp off bridge-fd 0 mtu 1400 # Public vSwitch auto eno1. iface eno1. inet manual vlan-raw-device eno1 auto vmbr_pub iface vmbr_pub inet manual bridge-ports eno1. bridge-stp off bridge-fd 0 mtu 1400 Node 2 — pve2 (only the address changes) auto eno1. iface eno1. inet manual vlan-raw-device eno1 auto vmbr_priv iface vmbr_priv inet static address 10.100.101.2/23 bridge-ports eno1. bridge-stp off bridge-fd 0 mtu 1400 auto eno1. iface eno1. inet manual vlan-raw-device eno1 auto vmbr_pub iface vmbr_pub inet manual bridge-ports eno1. bridge-stp off bridge-fd 0 mtu 1400 Node 3 — pve3 auto eno1. iface eno1. inet manual vlan-raw-device eno1 auto vmbr_priv iface vmbr_priv inet static address 10.100.101.3/23 bridge-ports eno1. bridge-stp off bridge-fd 0 mtu 1400 auto eno1. iface eno1. inet manual vlan-raw-device eno1 auto vmbr_pub iface vmbr_pub inet manual bridge-ports eno1. bridge-stp off bridge-fd 0 mtu 1400 Step 3 — Apply and Verify ifreload -a Check both bridges came up: ip addr show vmbr_priv ip addr show vmbr_pub vmbr_priv should show its 10.100.101.x/23 address. vmbr_pub should be UP with no IP — that's correct, the node intentionally has no address on the public bridge. Confirm MTU took effect: ip link show vmbr_priv | grep mtu ip link show vmbr_pub | grep mtu Both should say mtu 1400 . Step 4 — Ping Between Nodes Before Clustering Don't skip this. From node1: ping -c 4 10.100.101.2 ping -c 4 10.100.101.3 Both must respond before you touch pvecm . If Corosync starts before the private network is working, it falls back to binding on eth0 — silently, without an error. You end up with cluster heartbeat sharing your public management interface, and the only way you notice is when things go wrong under load. The pings take ten seconds. Do them. If either ping fails: check the VLAN ID in your bridge config against what Robot shows for the private vSwitch, confirm both servers are listed as members, and verify lsmod | grep 8021q shows the module loaded. Creating the Proxmox Cluster One Flag That Makes or Breaks This Everything in this chapter is straightforward except one thing: every pvecm command includes --link0 pointing to the private vSwitch IP. That flag is what binds Corosync's heartbeat to the private network instead of defaulting to eth0 . Skip it and Corosync uses the public interface — cluster heartbeat and SSH share the same NIC, and any issue with that interface takes down both simultaneously. The flag takes two seconds to type. Use it every time. Step 1 — Create the Cluster on Node 1 On node1 only . Replace with a short lowercase name: pvecm create --link0 10.100.101.1 pvecm status You should see 1 node, quorum achieved, and your cluster name. If it errors, check that vmbr_priv is up and has the right IP. Step 2 — Confirm Corosync Is on the Right Interface Five seconds that have saved me a painful undo more than once: grep "ring0_addr" /etc/pve/corosync.conf Must show 10.100.101.1 . If it shows the public IP instead, stop here and fix it before adding any other nodes. Unwinding a misconfigured Corosync bind address after two nodes have already joined is a lot more work than catching it now. Remove the cluster, fix the bridge, and recreate: pvecm expected 1 systemctl stop pve-cluster corosync pmxcfs -l Step 3 — Join Node 2 On node2 only : pvecm add 10.100.101.1 --link0 10.100.101.2 You'll be prompted for node1's root password. After it completes: pvecm status Two nodes will show, but the cluster is fragile right now — with only two nodes, losing either one drops quorum to zero. Add node3 immediately, don't leave it at two. Step 4 — Join Node 3 On node3 only : pvecm add 10.100.101.1 --link0 10.100.101.3 After that completes, check from node1: pvecm status The key line to look for: Votes: 3 expected, 3 total, 2 quorum Three votes, quorum at 2 — the cluster can lose one node and keep running. That's what you want. Step 5 — Check the Web UI Open the Proxmox web UI on any node's public IP (port 8006). All three nodes should appear in the cluster tree on the left with green status. Give it up to a minute if any node shows offline right after joining — the initial sync takes a moment. Quorum: Why Three Nodes and Not Two Two nodes is asking for trouble. Quorum requires a strict majority of votes, so two nodes means both must be up at all times — any single failure freezes the cluster. Three nodes gets you fault tolerance without needing an external quorum device (QDevice). It's the minimum sensible cluster size. Nodes Quorum needed Can lose 2 2 of 2 0 — any failure freezes the cluster 3 2 of 3 1 4 3 of 4 1 5 3 of 5 2 7 4 of 7 3 Stick to odd numbers when you can — 3, 5, 7. Even node counts give you the same fault tolerance as the odd number below them with an extra machine. Adding More Nodes Later The process is the same as joining node2 and node3. On the new server: install Proxmox per the first guide in this series , add both vSwitch bridges per Chapter 3 here (use the next sequential IP — 10.100.101.4 for a 4th node, and so on), then: pvecm add 10.100.101.1 --link0 Corosync updates the quorum vote count automatically. No config file edits needed. Deploying PfSense on the Cluster Why PfSense Is Part of This Setup The private vSwitch is a flat layer-2 network. VMs can talk to each other and to the cluster nodes, but they have no route out. PfSense is what changes that — it bridges the private vSwitch (LAN side) with the public vSwitch (WAN side), running NAT so every VM in the 10.100.100.x range can reach the internet through a single public IP. Without PfSense, the private network is useful for internal VM communication only. PfSense is not optional in this setup. What This Chapter Covers (and What It Doesn't) The full PfSense installation — downloading the ISO, creating the VM, running through the installer, and the initial web UI setup — is covered in detail in Public VM Connectivity on Hetzner via PfSense , specifically Method 3 (Hetzner vSwitch) . Go there if you haven't deployed PfSense before. This chapter only covers the interface assignments, IP values, and MTU settings specific to this cluster layout. The delta is small but the values matter. Step 1 — Create the PfSense VM on Node 1 Follow Public VM Connectivity on Hetzner via PfSense — Method 3 for the VM creation and install. When you get to the network interface step in the Proxmox VM wizard, add two NICs: NIC Bridge Becomes First vmbr_pub PfSense WAN Second vmbr_priv PfSense LAN During PfSense's first-boot console setup, it asks you to assign interfaces — assign the NIC on vmbr_pub as WAN and the one on vmbr_priv as LAN. Step 2 — Set MTU 1400 on the WAN Interface In the PfSense web UI: Interfaces → WAN → set MTU to 1400 . Save and apply. PfSense doesn't inherit the MTU from the Proxmox bridge — you have to set it explicitly. Without it, large packets on the WAN side hit the VXLAN overhead ceiling and get fragmented. The symptom is exactly what you don't want: everything seems to work until it doesn't, and the failures are inconsistent enough to chase for a while before landing on MTU as the cause. Step 3 — Set MTU 1400 on the LAN Interface Interfaces → LAN → MTU 1400 . Save and apply. Same reason as WAN — the LAN side also goes through a vSwitch bridge. Setting it here also lets PfSense clamp TCP MSS correctly in its firewall rules, which prevents large TCP sessions from stalling on connections that cross the MTU boundary. Step 4 — Configure the LAN IP If it wasn't set during the console setup, assign the LAN interface: IP: 10.100.100.1 Subnet: /23 This is the default gateway for everything in the 10.100.100.0/23 range — VMs in the .100.x pool and cluster nodes in .101.x . Step 5 — Configure the WAN IP Assign the WAN interface the first usable IP from your Hetzner additional subnet, with the gateway Hetzner provides for that subnet (visible in Robot under the subnet details). Step 6 — DHCP for the VM Pool (Optional) Services → DHCP Server → LAN — enable it, set the range to 10.100.100.2 – 10.100.100.254 . That covers the VM pool and deliberately excludes the node addresses in .101.x , which should always be static. If you'd rather assign VM IPs manually, skip this. Step 7 — Smoke Test Spin up a minimal test VM on vmbr_priv and run: ping -c 4 10.100.100.1 # PfSense LAN ping -c 4 8.8.8.8 # Through PfSense to the internet curl -s ifconfig.me # Should return PfSense's WAN IP, not the node's PfSense Is Single-Instance — Know the Implication Right now PfSense runs on one node. If that node goes offline, VM internet access goes with it. The cluster itself stays up — losing one node still satisfies the 2-of-3 quorum, so Proxmox keeps running and the other VMs keep running. But any VM that needs outbound connectivity is cut off until PfSense restarts or the node recovers. The solution is redundant PfSense with CARP — two PfSense instances sharing a virtual IP, one taking over automatically when the other disappears. That's a future article. VM Network Setup Creating a VM on the Private Network With the cluster up and PfSense running, adding a VM that can reach the internet is straightforward. The two things to get right at the Proxmox level are the bridge and the MTU — everything else is standard VM setup. Step 1 — Network Interface in the VM Wizard In the Proxmox web UI, create a VM on any node. In the Network step: Field Value Bridge vmbr_priv MTU 1400 VLAN tag (leave empty) The MTU here and the MTU on the bridge in Chapter 3 are two separate settings that both need to be 1400. The bridge MTU caps the frame size leaving the host. The VM NIC MTU tells the guest OS what to advertise to applications. Leave the VM NIC at the default 1500 while the bridge is at 1400 and small packets work fine while large transfers stall — the kind of issue that's annoying to diagnose because "the network works" until it suddenly doesn't. Step 2 — IP Configuration Inside the VM If PfSense DHCP is enabled, the VM picks up an address automatically with the right gateway. For static assignment: IP: 10.100.100.x (anything in .2 – .254 that's not taken) Mask: 255.255.254.0 (/23) Gateway: 10.100.100.1 DNS: 10.100.100.1 (or a public resolver) Step 3 — Verify ping -c 4 10.100.100.1 # PfSense LAN — bridge and vSwitch working ping -c 4 8.8.8.8 # Internet — PfSense NAT working curl -s ifconfig.me # Returns PfSense's WAN IP The 30-VM Limit — Keep Count Hetzner doesn't tell you when you're near the 32 MAC address limit. You'll add a VM, the network interface will come up clean in Proxmox, and then it just won't pass traffic — no error, nothing obvious in logs. If you're not tracking VM count per node, that failure mode takes a while to connect back to MAC exhaustion. With the two vSwitch bridges on each node, you have 30 VMs per node . That's 90 across the full 3-node cluster. When any single node approaches 30, move the next VM to a node with headroom or add another node. If you genuinely need more than 30 on one machine, contact Hetzner support — they can raise the limit on a per-server basis. What About vmbr_pub? VMs that need a direct public IP — not NATted through PfSense, but their own IP from the Hetzner subnet — would attach to vmbr_pub instead and get an IP from the additional subnet. That configuration, including routing and firewall setup, is covered in a future article. Why we designed it this way The cluster makes a few structural choices on purpose: Two vSwitches, not one. One private (cluster heartbeat plus VM east-west traffic), one public (inbound). Keeping the latency-sensitive cluster chatter on its own network means a busy public link can't disturb it. Corosync on the private network. Corosync is the cluster's heartbeat — how nodes agree on who's alive. It is sensitive to latency and jitter . Put it on a quiet, dedicated link and a traffic spike on the public side can't trigger false "node down" events (and the fencing dramas that follow). pfSense as the cluster-wide gateway. With one gateway serving VMs on any node, a VM can live on — or migrate to — any host and still reach the world through the same controlled edge, without rewiring its networking. Plan density around the per-node MAC ceiling. The provider caps how many MAC addresses a host may present. On a virtualisation host that ceiling — not CPU or RAM — can be the real limit on how many VMs you can run. Design with the number in mind. And the bigger "why a cluster at all": three nodes give you live migration, a path to high availability, and one management plane across all of them — at the cost of more networking complexity, which is what this book is mostly about taming. The mindset: separate the traffic that must stay healthy (cluster heartbeat) from the traffic that's noisy and bursty (public/VM). Most cluster instability traces back to those two sharing a path. Decide your real limits (here, MAC count) up front, instead of discovering them when you can't start VM number N. Lessons learned & gotchas MTU is the one that will haunt you. A vSwitch with VXLAN encapsulation adds overhead to every frame. Leave the VM/bridge MTU at the default 1500 and you get the classic, maddening symptom: ping works, but SSH hangs and TLS half-loads — small packets pass, large ones silently vanish. Lower the MTU to leave room for the encapsulation header and it all comes back. If "small things work, big things don't," suspect MTU before anything else. Pin Corosync to the private vSwitch explicitly. Don't let it auto-pick and end up on the public interface — that's how a public-traffic spike turns into a cluster falling over. Three nodes is not an accident. Quorum needs an odd number to avoid split-brain. A two-node cluster needs a tie-breaker (a qdevice) or it will eventually deadlock on "who's in charge?" Respect the per-node MAC ceiling. Know the number before you promise yourself a hundred VMs per host; it may be your true ceiling long before CPU or RAM is. Rehearse a node failure in daylight. Pull a node on purpose, watch quorum and migration behave, and fix what doesn't — before you're relying on HA during a real 3am outage. Lesson in one line: cluster pain is almost always networking pain — MTU, heartbeat isolation, and quorum. Get those three right and the cluster is boring (which is the goal).