//architecture & operations deep dive

Five-Node Proxmox Infrastructure

Five used office machines turned into a platform that routes, resolves, proxies and backs up the network it sits on — built deliberately as standalone nodes rather than a cluster, and documented decision by decision, including the ones that were rejected.

RoleArchitect & operator
Timeline2026 — present · in service
PlatformProxmox VE · OPNsense · Debian
Hardware5 × Dell OptiPlex SFF, ~$260

01Context & constraints

Five second-hand Dell OptiPlex small-form-factor machines, bought for roughly $260 CAD in total, running Proxmox VE. The goal was to learn infrastructure by operating it rather than reading about it — on hardware where a mistake costs an evening instead of an invoice.

What turned it from a pile of machines into a design problem was a specific failure. The house router's firmware had no field for handing out a DNS server, so every device on the network silently resolved through the ISP instead of the lab's own filtering resolver. It had been that way for weeks before anyone noticed, because the fallback worked. Fixing that properly is what eventually justified the lab running its own router.

Constraints that shaped the design

  • It lives in a bedroom. Five machines running continuously is about 150 W — a small space heater that never switches off, plus five fans and three spinning disks. Nodes have to be able to sleep and wake on demand.
  • No ethernet in the room. The router is on a different floor, the lab can't move, and — as it turned out — neither can the router.
  • Unknown hardware provenance. Five machines from a reseller, sold as wiped, with no service history and disks of unknown age.
  • The household can't be collateral. Anything that breaks in the lab must not take the family's internet down with it.

02Inside the lab

The running system — the aggregated node view, the router the lab built for itself, a node's guest and storage layout, the backup datastore, and the resolver carrying real traffic.

Datacenter Manager — five standalone nodes and every guest in one view, with none of the quorum a cluster would demand.
fig. 01 Datacenter Manager — five standalone nodes and every guest in one view, with none of the quorum a cluster would demand.
OPNsense — the lab’s own gateway. WAN and LAN ride physically separate NICs; the gateway address and version detail are redacted.
fig. 02 OPNsense — the lab’s own gateway. WAN and LAN ride physically separate NICs; the gateway address and version detail are redacted.
pve1 — DNS, reverse proxy and the manager VM, with guest storage on its own NVMe pool separate from the boot disk.
fig. 03 pve1 — DNS, reverse proxy and the manager VM, with guest storage on its own NVMe pool separate from the boot disk.
Backup Server — a 290 GB datastore on a dedicated node, with the router and manager VMs backing up nightly.
fig. 04 Backup Server — a 290 GB datastore on a dedicated node, with the router and manager VMs backing up nightly.
AdGuard Home — 13,316 queries in 24 hours, 21% blocked, 10 ms average. The evidence the design is actually carrying traffic.
fig. 05 AdGuard Home — 13,316 queries in 24 hours, 21% blocked, 10 ms average. The evidence the design is actually carrying traffic.

// redacted regions are masked, not blurred — the underlying pixels are destroyed.

03Getting on the network

The single longest-running blocker in the build, and none of it was technical. The interesting part is not the answer — it's the four options that had to be eliminated to arrive at it.

Move the machinesThe right answer in the abstract, and free. Rejected for household reasons — and it would also have solved the heat and noise problem before it existed.
Move the routerDepends entirely on how the internet arrives. On DSL the modem can often move to any phone jack. It's fibre — the ONT is fixed at the wall penetration where the installer drilled. Dead end.
MoCAGenuinely the elegant option: gigabit over existing coax, no drilling. Real effort went into it before discovering the “coax jack” in the room was an RJ11 phone jack. MoCA needs an F-connector at both ends. Dead end.
Wireless bridgeRejected on principle, not price. Proxmox builds its networking around a Linux bridge so guests share the host NIC, and wireless interfaces cannot be bridged — 802.11 does not let a client relay frames for other MAC addresses.
Run a cable150 ft of flat Cat6, $35.99. Flat cable is about a millimetre thick and tucks under baseboard trim in a way round cable can't. Pure copper rather than copper-clad aluminium, because CCA gets flaky over long runs — which is exactly the failure a 150 ft run would expose.
The $5 mistake worth writing down

Two RJ45 “splitters” were bought alongside the cable. They are not switches — they're passive adapters that split eight wires into two four-wire pairs, so both links cap at 100 Mbps, because gigabit needs all four pairs. They also only work as a matched pair, one at each end of a single run.

A first attempt used a 75 ft cable that reached — stretched across the floor, untucked. Routed properly along baseboards and around door frames, real length runs 20–30% over the direct path. An RJ45 coupler to join two shorter cables was considered and rejected: it works, but it adds a connection point that can work loose and produce intermittent link failures, which is the kind of fault that gets misdiagnosed as software for hours. A single run was worth the $36.

04Node roles, not a cluster

Clustering gives a single web UI but requires quorum: enough nodes online at all times, or the survivors go read-only. For a lab where machines are deliberately powered down, that is the wrong trade — and removing a node from a cluster later is painful.

Proxmox Datacenter Manager solves the actual problem — one dashboard across independent nodes — without the constraint. It manages remotes purely over the API, covering monitoring, start/stop, migration and snapshots, while deep per-node configuration stays in that node's own interface.

Roles follow the binding resource

Faster CPU + NVMe

Takes the latency- and disk-sensitive services: the resolver, the reverse proxy and the manager VM. Its hypervisor boots from a separate SSD, so it can be reinstalled without touching guests and boot I/O never contends with VM I/O.

Most RAM, slower disk

Takes the memory-hungry, disk-tolerant workloads — and doubles as the node that can be broken freely, because nothing depends on it.

Least RAM → backups

Counter-intuitive, and deliberate. Backup Server is the workload least sensitive to RAM: roughly 2 GB for the service plus about 1 GB per TB of datastore. 16 GB is four times what it needs.

Mechanical disks → targets

Sequential writes are the one workload spinning disks are genuinely fine at. Two HDD nodes means one holds the datastore and the other takes a periodic sync — two copies on two independent machines.

Why ext4 and not ZFS

The “Backup Server needs lots of RAM” folklore comes from ZFS: its ARC will happily consume 8 GB or more. On a single disk with no redundancy, ZFS buys nothing here, so the datastore runs on ext4 and the RAM estimate holds. The consequence was worth naming: that node stops being expendable, because it has to be up when scheduled jobs fire — so the scratch role moved elsewhere.

Design choice · not rebalancing the RAM

Memory is distributed unevenly across the nodes and the arithmetic to even it out works. It was left alone. Nothing in the plan is memory-constrained except the node that already has the most, so moving capacity to a node running a firewall and a container optimises nothing. A rebalance backed by evidence is worth doing; one backed by tidiness is not.

05Taking over the network

The lab now runs its own gateway. An OPNsense VM holds the LAN gateway address and serves DHCP pointing every client at the lab's resolver — the field the house router's firmware never had, and the reason the whole exercise was worth doing.

A router VM needs two interfaces

A NIC audit across all five nodes turned up an Intel I210-T1 server adapter sitting unused in one machine's PCIe slot, never cabled since the day it arrived. Transplanted into the node that would host the router, it gave the VM two physically separate interfaces at zero cost — WAN on the add-in card, LAN on the onboard NIC.

The move was confirmed at the hardware level rather than assumed: the card enumerated under lspci -nnk with the igb driver bound, initialised cleanly in dmesg, and its MAC address travelled with it — the same address that had been the donor node's unused second interface appeared as the recipient's new one. IOMMU groups were checked at the same time, so PCIe passthrough stays available as an option later.

The cutover, and the address that didn't move

BeforeHouse router holds the LAN gateway address. No DNS field in its DHCP settings, so clients resolve through the ISP.
AfterHouse router moves to its own WAN subnet and becomes a modem. OPNsense takes the same gateway address the lab always used.
ResultNothing needed editing. Not a static IP, not a proxy route, not a DNS rewrite, not a backup storage definition. The gateway address didn't move — only what answers on it.
Design choice · sequencing

The cutover was built last, not first. The second NIC, the bridge, the VM, its firmware and the interface assignment were all in place and verified before anything touched the address the lab was reached on — so the one step that could sever remote access was taken with a lifeline running and a rollback snapshot already taken. Doing it first would have meant configuring a router through the network it was breaking.

One detail that looks arbitrary and isn't: the new WAN subnet deliberately avoids OPNsense's own factory-default LAN range. “Reset to factory defaults” is one keystroke away on the console menu, and a reset that lands the firewall on the same subnet as the upstream router turns a recoverable mistake into a confusing one.

Household Wi-Fi was left on the WAN side throughout, unaffected by anything happening in the lab. That was the original objection to running a homemade router at all, and it was resolved by topology rather than by not building it.

06Names & certificates

Services are reached by name over trusted HTTPS, with no ports to remember and no certificate work when a new one is added.

ResolverAdGuard Home in a container, filtering for the whole LAN and forwarding upstream over DoH. The encryption happens on the upstream hop, because the threat being addressed is the ISP reading queries — not the LAN.
NamingOne wildcard rewrite, not one record per service. Every internal name resolves to the proxy, so adding a service is a proxy edit and nothing else. A per-service record scheme would need a DNS change every time.
ProxyCaddy in a container, routing by requested name to the right host and port. Chosen over nginx for automatic certificate handling — the thing that otherwise has to be solved per service, forever.
CertificatesAn internal certificate authority issues for every internal name, with the root installed once on each client. A padlock everywhere, and no public DNS record or inbound exposure to get one.
Remote accessTailscale as a subnet router, so the whole lab is reachable from a phone on cellular without opening a single inbound port.
Known consequence, stated rather than discovered

Concentrating the resolver, the proxy and the certificate authority on one node means every internal name in the lab depends on that node being up. That is a real single point of failure, and naming it is what makes the second resolver instance a planned piece of work rather than an outage waiting to teach the lesson.

07Backups that restore

A backup that has never been restored is an assumption. The point of this section is the last step, not the first.

  • Dedicated node. Backup Server runs as a VM on the node whose disk is a mechanical drive, because backups should never live on the machine being backed up.
  • A scoped token, not root. Each node authenticates with a token holding only datastore rights, rather than the root account. An automation credential should be able to do less than the human it belongs to.
  • Retention as policy, not per-job. Prune and garbage collection run on the datastore itself on a schedule, so jobs inherit one retention policy rather than each carrying its own drifting copy.
  • The restore test. A container was restored to a throwaway ID, verified, and destroyed. Six seconds of work that converts the entire backup design from theory into fact.
  • Verification against bit rot. The datastore lives on a drive with more than 50,000 power-on hours. Scheduled verify jobs are what catch a corrupted chunk before a restore needs it.
Design choice · attach first, prove immediately

Attaching backup storage to a node and creating a backup job are two separate actions, and it is entirely possible to have the first without the second — storage showing active, no job configured, nothing ever written. Every job was run once manually on creation rather than trusting the schedule to prove itself overnight.

08Trusting used hardware

Five machines arrived with nothing known about them beyond a seller's description. Two assumptions in that description turned out to be wrong, and finding out cost nothing because it was checked first.

“Wiped” from a reseller means nothing

The first machine booted straight into a Windows login screen still carrying its previous corporate account — the original install intact, and the disk not even encrypted. Nothing was opened and the whole disk was overwritten, but it reset two working assumptions: verify what a reseller tells you, and read specifications out of firmware rather than out of an operating system that may not be yours to log into.

Two of five disks were already failing

Every disk was SMART-audited before anything was committed to it. Same model family, same era, wildly different health:

Power-on hoursReallocatedPendingATA errorsVerdict
42,241000Clean — in service
50,250000Clean — in service
59,364000Clean — in service
45,8501361636Degrading — retired
58,66218401,229Failing — retired

Power-on hours turned out to predict nothing — the highest-hour drive is clean and a lower-hour one is the worst of the five. Reallocated and pending sectors are the signal, and both bad drives were found before they could take data with them rather than during a restore.

09Diagnosis log

Twenty-nine problems were logged during the build with their reasoning, not just their fixes. Six that generalise beyond this lab:

A vague UI error, and the specific one underneath it

The manager could not authenticate to a node. The web interface offered only authentication failed. The node's own service journal had the real answer:

# on the target node, not the thing showing the error
journalctl -u pvedaemon --since "15 minutes ago" | tail -30

authentication failure; user=root@pam@pam msg=no such user ('root@pam@pam')

The realm was being appended twice — typed into the username field and selected from the realm dropdown. Worth noting which service logged it: the proxy daemon handles the TLS front end and had nothing relevant, while the daemon doing the actual credential check had the exact string. When a management UI gives a vague error, the service journal on the target usually has the specific one.

A credential with the right permission that could do nothing

Adding the backup datastore to a node failed with Cannot find datastore … check permissions and existence — misleading, because it plainly existed. Running the backup client directly gave the real error: a missing permission on the datastore path.

The cause is a design property rather than a bug. A token's effective rights are the intersection of its own ACL and its parent user's. The token had been granted datastore rights; the user it belonged to had none. Intersect anything with nothing and the result is nothing — and the permission listing looks correct, because it is correct, just insufficient alone.

A NIC that enumerated perfectly and still showed no link

The transplanted card checked out completely — listed by lspci, driver bound, clean initialisation with interrupt vectors and queues. Cable in a switch with seven other working ports. And no link, at either end.

# the interface was administratively down, and the driver
# powers the PHY down with it — no autonegotiation, no link LED
ip link set enp2s0 up
# → Link detected: yes, Speed: 1000Mb/s

A dark switch port feels like decisive physical-layer evidence and it isn't: a powered-down PHY looks exactly like a severed cable. The distinguishing test is watching the kernel log live while unplugging the cable — a live NIC logs each link event, silence means it genuinely can't see the wire. But that only works if the interface is up first, which is the same precondition the original symptom violated.

Two commands that answer different questions

nslookup service.internal 192.168.x.x   # does the resolver work?
nslookup service.internal                # am I actually using it?

The first succeeded, the second didn't. Passing a server explicitly bypasses the system configuration entirely, so it will pass on a machine that never sends it a real query. The desktop's Wi-Fi adapter had been configured correctly and the Ethernet adapter — the live one — had been on the router's DNS for weeks, since an outage when it was reverted to DHCP and never switched back.

Nothing looked broken because the router also hands out a public filtering resolver, so ad blocking kept working the entire time. The two-layer design did exactly what it was built to do, and the fallback was invisible precisely because it worked. Good design, bad observability — and the concrete argument for the lab serving its own DHCP rather than configuring DNS device by device.

An error message specific enough to skip three hypotheses

From 192.168.x.x icmp_seq=1 Destination Host Unreachable

A node wouldn't wake, and the instinct was to suspect a mis-transcribed MAC address. But Destination Host Unreachable from the sending host means the ARP request went unanswered — the machine isn't on the network at all. That eliminates firewalls, services and configuration in one line and points at power or link. The cause was an unseated cable; with no link the NIC can't receive a magic packet either, so the wake failure was a symptom of the same fault rather than a second problem. A plain timeout would have been ambiguous. This wasn't.

The installer that assigned the interfaces backwards

The firewall installer's defaults came up with WAN and LAN reversed — the interface connected to the upstream router assigned as the internal network and vice versa. Caught before the cutover because the assignment was checked against which virtual NIC mapped to which bridge, rather than trusted. On the same box the installer never prompted for a root password, leaving the appliance on its factory default: two failures in one build that only get found by verifying defaults instead of assuming them.

10Key decisions, condensed

Every choice below traded convenience or tidiness for a property the lab actually needed — recoverability, safety, or the freedom to switch a machine off.

Standalone, not clustered

A cluster goes read-only without quorum. Nodes here are meant to be powered down. One dashboard over the API gives the benefit without the constraint.

Roles follow the resource

Fast disk takes latency-sensitive services, most RAM takes the memory-hungry ones, mechanical disks take backups. Not a ranking of the machines.

ext4, not ZFS

ZFS earns its RAM with redundancy. On a single disk it buys nothing, and the ARC is where the “backups need lots of memory” folklore comes from.

Cutover sequenced last

Every dependency verified before touching the address the lab is reached on. The one irreversible step ran with a lifeline and a rollback snapshot.

One wildcard, not many records

Every internal name resolves to the proxy, so a new service is a proxy edit and never a DNS change.

Verify before trusting

Disks SMART-audited before use, the NIC transplant confirmed by its MAC, backups proven by restoring one. Defaults are checked, not assumed.

11Stack & takeaways

5
standalone nodes, no quorum
29
problems diagnosed & logged
2/5
disks caught failing before use
0
addresses changed in the cutover

Full stack

Proxmox VEProxmox Datacenter ManagerProxmox Backup ServerOPNsenseDebianLXCKVM / QEMUAdGuard HomeCaddyTailscaleKea DHCPDNS-over-HTTPSInternal CA / TLSWake-on-LANSMART / smartctlethtoolBash

What I'd carry forward

The lever wasn't any single service — it was refusing to trust anything that hadn't been checked. Disks audited before data went on them, a transplanted card confirmed by its hardware address rather than by assumption, an installer's defaults read back instead of accepted, and a backup proven by restoring it. Every one of those was cheap at the time and would have been expensive later.

The second lever was writing down the decisions that went the boring way. Not clustering, not rebalancing memory that nothing was waiting on, not buying hardware because buying feels like progress. A change backed by evidence is worth making; one backed by tidiness is not — and a build log is what makes the difference visible six months later.