How I built my homelab: two old Dells, pfSense and a single cable
Two old Dell servers, a virtualized pfSense and a single cable to the ISP box: the order I built my homelab in, and the traps that taught me the most.
8 min read#homelab #proxmox #pfsense #networking #systemd
At the end of September, I had two old Dell servers and an ISP box. Three weeks later, they run this site, the SIEM of my degree project, my AI assistant and a Minecraft network for friends. The architecture is described on the project page; here I tell the order I built the homelab in, and the traps that taught me the most.
The hardware: an R630, an R320 and six-year-old disks
The main node is a PowerEdge R630: two Xeon E5-2630 v3, 32 threads, 64 GB of RAM at first, 128 GB since October 4. Its 10k SAS disks showed about 51,600 power-on hours, six years. The second, a single-Xeon PowerEdge R320, keeps the backups; its system disks are seven years old.
The R320 greeted me with this message, and sometimes refused to boot:
CPU MEM VTT PG Voltage is outside memory range. Reseat CPUMemtest86+ passed without an error. No surprise: it tests memory once the machine has booted, while the alarm fires at power-on. That left the iDRAC's hardware log, the SEL, which showed no error anymore. It was full:
ipmitool sel info # 1024 entries, 0 bytes free: nothing new gets logged
ipmitool sel elist > sel-pve2-20260925-1427.txt
ipmitool sel clearA full log with no errors proves nothing. I copied it, cleared it, and I read it again after every boot. The R320 has run on two DIMMs, 16 GB, ever since, and I still haven't settled between a DIMM, a slot and the voltage regulator.
The cluster before the firewall
I followed a set order: the R320's BIOS and iDRAC updated, Proxmox VE on both machines, a direct cable between them, the ZFS pools, the cluster, and pfSense last. Changing a node's address once it is in a cluster is painful, so I put corosync's main link on the direct cable, with addresses that would never change. During the move behind the firewall, that link stayed intact.
Node management, on the other hand, got stuck: Proxmox reaches each node at the address its name resolves to, and the R630 could no longer manage the R320 (No route to host). Once /etc/hosts was fixed:
systemctl restart pve-cluster # it kept the old address cached
pvecm updatecerts --force # new certificate for the new addressWith no third machine for a QDevice, the R630 has two votes out of three: it stays quorate on its own, the R320 doesn't.
A single cable to the box, no access without the VPN
The ISP box is plugged into the R630 only. On that side, the Proxmox bridge has no address for the host: it carries the WAN of pfSense, which runs as a VM. The pfSense LAN is a VLAN-aware bridge, extended to the R320 over the direct cable, and each zone (services and AI, games, lab, coursework, the site's DMZ) has its own VLAN there. The box's DMZ sends everything to pfSense.
The tricky moment was removing the nodes from the box network: after that, no VPN meant no administration at all. So every change to /etc/network/interfaces came with a safety net, a job that puts the old config back after three minutes unless I cancel it. It lives in systemd, not in my SSH session: if the connection drops, it still runs. The idea, simplified:
cp /etc/network/interfaces /root/interfaces.bak # then edit the file
systemd-run --on-active=180 --unit=net-rollback \
/bin/sh -c 'cp /root/interfaces.bak /etc/network/interfaces && ifreload -a'
ifreload -a
systemctl stop net-rollback.timer # only if the session still answersSecond precaution: a break-glass port on each node, unplugged, with a static address and no gateway, to plug a PC into if pfSense goes down. Without the setting below, Linux keeps the route of an unplugged port, and the R320 would have sent part of its traffic into the void:
# /etc/sysctl.d/90-linkdown.conf
net.ipv4.conf.all.ignore_routes_with_linkdown = 1
net.ipv4.conf.default.ignore_routes_with_linkdown = 1The only accidental exposure came later. Through the DMZ, internet traffic arrives with the box's local address: port forwards meant for the box network therefore made my dashboard reachable from the internet. I removed them right away, and I now test every opening from outside.
A backup you have never restored is just a hope
Every night, the configuration of both nodes, /etc/pve included, goes to the R320. vzdump jobs back up pfSense weekly and the important containers daily, and Proxmox warns me of a failure through ntfy. I restored the first pfSense backup the same day:
qmrestore /var/lib/vz/dump/vzdump-qemu-100-2026_09_25-00_31_16.vma.zst 190 \
--storage tank --unique 1Four seconds for a 20 GB disk that is 92% empty. --unique gives new MAC addresses, but the copy keeps the IP addresses of the real firewall, including the gateway of the whole network: started as is on the same bridges, it would have fought the real one for them. So I unplugged its two virtual NICs (link_down=1) and turned off its autostart before launching it. It booted to the pfSense menu, then I deleted it; the real one never stopped answering.
The real reboot
On October 4, I shut the R630 down to double its RAM and add a third pair of disks to the ZFS pool. Beforehand, I checked the guests' boot order (pfSense first, with a delay) and that nothing was running by hand.
Everything came back except two services in the AI container. nginx had started before DNS answered:
host not found in upstream "api.openai.com:443"A small Python web server, started before its address was assigned (Cannot assign requested address), was restarted five times in a fraction of a second, then given up on. That's systemd's default behavior: 100 ms between restarts, at most five starts in ten seconds. Proxmox's boot order guarantees that pfSense starts first, not that the DNS chain already answers. Both services now retry every five seconds, with no limit:
# /etc/systemd/system/nginx.service.d/redemarrer.conf
[Unit]
StartLimitIntervalSec=0
[Service]
Restart=on-failure
RestartSec=5sWhat runs on it today
Everything fits on the R630: 12 VMs and 14 containers, including a template and a test container that is switched off, for 105 GB of RAM used out of 126. The R320 has been off since the end of September: nothing depends on it anymore, apart from the backups. By zone:
- The core.
fw-01, pfSense as a VM: firewall, NAT and WireGuard VPN.dns-01: Pi-hole and a recursive Unbound, for the homelab and the house alike.mon-01: Prometheus, Grafana and Alertmanager, which alerts me through ntfy.homepage-01: the dashboard, just below. Andmodele-debian, the unprivileged template every container is cloned from. - This site.
web-01, a VM alone in its DMZ, published through the Cloudflare tunnel. - AI.
hermes-01runs the Hermes agent and is the single entry point for everything AI: nginx, allowlist, keys.llm-01hosts FreeLLMAPI, a router to free models;voix-01, the voice engine;maj-01, the signed updates of the HermesIAssistant app. - Games. The Minecraft network for friends: a Velocity proxy, the only way in, a lobby with a compass to pick a server, vanilla survival, All The Mods 11 and DeceasedCraft. Five containers.
- The lab.
siem-01, the SOCFortress SIEM (Wazuh, Graylog, TheHive, Shuffle, Velociraptor), 32 GB of RAM on its own. Around it, what feeds it: a deliberately vulnerable target (DVWA), a Debian client, a Windows 11 client and an Active Directory on Windows Server 2025. - Coursework. Five Windows VMs for a long-running lab of my studies: a domain controller, a file server, a dedicated admin workstation and two client PCs.
My favorite part is still the home page. It started as Homepage, which I styled until I hit its grey background, coming back after every load. I replaced it with a custom page, served by nginx in the same container. Every 2 seconds, a Python collector reads node_exporter on the nodes; every 10 seconds, the Proxmox API, with TLS verified against the cluster's CA; every 30 seconds, Pi-hole. It writes it all to a data.json that the page reads again. A node silent for 10 seconds goes "Offline" instead of showing frozen values.
The Panel's header, the Hermes assistant and the main node
Further down, each zone has its cards. The Minecraft one shows the connected players, each server's load and the survival world's day counter, read over RCON.
The Panel's Minecraft zone
What's still missing
Everything is in the same room: no off-site copy, no UPS, no third vote for the quorum. A fire or a power surge, and the backups go with the servers. And as long as the R320 stays off, the nightly copy has nowhere to go. That's the next project; looking back, I would have planned it before having anything to lose.
What I would do the same way: the order, and the journal. I run the homelab with Claude Code as the operator: it executes over SSH, I decide and approve every risky change. Every intervention ends up in a journal versioned with git, 252 entries so far. That journal is what let me write this article, error messages included. The site itself joined the homelab on October 14, without opening a single port.