Proxmox Homelab

The infrastructure that hosts my projects: two Dell servers in a Proxmox VE cluster behind a virtualized pfSense, a network split into VLANs, backups, monitoring and a log of every intervention.

Overview

My homelab is the infrastructure that runs my other projects: the SIEM from my study project, the server behind the Hermes assistant, a Minecraft network for friends and everyday services. It rests on two Dell servers, a PowerEdge R630 and a PowerEdge R320, joined in a Proxmox VE cluster. A virtualized pfSense separates it from the ISP box's network, and every intervention is recorded in a git repository.

Two unequal servers, a deliberate quorum

The data lives on ZFS, as mirrored pairs referenced by their by-id: one pool per node for the machines, a large mirror on the R320 for backups. I doubled the R630's RAM, added a third pair to the pool with zpool add and raised the ARC cache ceiling, which the installer does not readjust.

With no third machine for a QDevice, I gave two votes to the R630, the main node, in corosync.conf, then stopped corosync on the R320 to check that the main node stays quorate on its own. I created the cluster before the firewall, with the direct cable between the servers as the first link, so I would not have to change the nodes' addresses later. Before installing Proxmox on the R320, I updated its BIOS and iDRAC.

A single cable to the ISP box

The box is connected to the R630 only, on a bridge with no host address that serves as the pfSense WAN. The pfSense LAN is a VLAN-aware bridge, extended to the R320 over the direct cable. In normal operation, the nodes have no address on the box's network: I manage them through a WireGuard VPN. Each node keeps an unplugged port, with a fixed address and no gateway, to take back control if the pfSense goes down. I first enabled ignore_routes_with_linkdown: without it, Linux would have kept the route of a port with no cable.

Behind the pfSense, each zone is a VLAN: services and AI, games, lab, Master's practical assignment. The rules follow the same model everywhere, tested with a disposable Alpine container: DNS to the pfSense, a logged block of an alias grouping all private networks, then internet. DNS goes through Pi-hole and a recursive Unbound; the pfSense forwards only to Pi-hole since I turned off the option that reinjected the box's DNS servers and bypassed the blocking.

Part of the pfSense is configured through PHP scripts that call its internal API. Most of them are idempotent: they only write the configuration when something changes.

$voulu = ["format" => "rfc3164", "ipproto" => "ipv4"];
$change = false;
foreach ($voulu as $cle => $val) {
  if (config_get_path("syslog/$cle") !== $val) {
    config_set_path("syslog/$cle", $val);
    $change = true;
  }
}
if ($change) {
  write_config("SIEM : journaux distants");
  system_syslogd_start();
}

Services that come back on their own

My rule: an LXC container when possible, a VM only when needed (FreeBSD for the pfSense, Windows and deliberately vulnerable lab targets, the SIEM). Each service container is cloned from an unprivileged Debian template, tagged by zone and described in a Proxmox note.

The boot order starts the pfSense first with a delay, then DNS, monitoring, games and AI. The Minecraft servers have a systemd Restart=always drop-in: after a kill -9, the lobby came back on its own. During the full reboot after the RAM upgrade, two services in the AI container started before DNS or their address were ready, then gave up. They now retry every 5 seconds.

Back up, restore, get notified

Every night, sauvegarde-config.sh archives the configuration of both nodes, including /etc/pve, to the R320. vzdump jobs back up the pfSense weekly and the important containers daily. I tested restoring the pfSense into an isolated VM, with network cards unplugged and new MAC addresses, then deleted the copy.

Prometheus and Grafana monitor the nodes. Proxmox notifications go to ntfy, as do Alertmanager's alerts on the system partition: 80%, 90%, and full within 24 h according to predict_linear. I replaced Homepage with a custom dashboard: its collector.py reads node_exporter every 2 seconds and the Proxmox API every 10 seconds, with TLS verified against the cluster's authority, then writes data.json atomically.

What the log taught me

I run the homelab with Claude Code as the operator: it executes over SSH, I decide and authorize risky changes. The homelab-doc skill requires checking the state by command, then updating the sheets, the network plan and the log before closing a task. generer-site.py turns all of it into a self-contained local page, without key or secret files.

A WireGuard handshake does not prove access: a return route was missing on a node. A full hardware event log records nothing more, so the absence of errors proved nothing. Behind the box's DMZ, internet traffic can arrive with a local address: I removed port forwards that exposed the dashboard, and I now test every opening from outside. Still to do: an off-site copy of the backups and a real third vote for the quorum.