Back to index
02Infrastructure

Homelab

A Proxmox server I built from the ground up to actually understand hypervisors, storage and networking. It now runs 12+ self-hosted services across 11 isolated containers — DNS filtering, photo backup, a password manager, automation, monitoring with phone alerts — reachable from anywhere through Tailscale with zero ports open to the internet and a default-deny firewall in front of everything.

RoleDesign, build, ops
StartedAug 2026
Nodes1 live — laptop node pending
Status12+ services live

Timeline

Click a stop to open the chapter
Aug 2026

First boot

The main node is an i5-10400F desktop (hostname mohammad-pve) running Proxmox VE 9 — my first real hands-on with a hypervisor instead of just reading about one.

—~15GB RAM, managed over SSH
—Learning VMs vs. containers by actually running both
—Everything driven from the command line, no GUI shortcuts
Aug 2026

Storage layout

Split the node's disks into two ZFS pools: a fast SSD pool for VM and container state (the "brains" of services), and a striped HDD pool for bulk data like media and files. Some of that storage didn't come from a store — I stripped an SSD and a 2.5" HDD out of an old laptop and installed them straight into the node.

—ssd-pool — fast tier for app databases and container disks
—hdd-pool — two drives striped for capacity, ~1.35TB usable
—The SSD and one of the hdd-pool drives were salvaged from a dead laptop instead of bought new
—Striped means no redundancy — fine for replaceable media, not for anything I can't lose
Sep 2026

DNS filtering

Started with AdGuard Home as the network's DNS-level ad blocker. It didn't handle parental-control filtering well, so I fully destroyed that container and rebuilt DNS filtering on Pi-hole, reusing the exact same static IP so nothing else on the network needed to change.

—~4.4 million domains loaded across ad, malware, tracking and adult-content lists
—Verified live: blocked domains resolve to 0.0.0.0, normal sites resolve fine
—First service other people in the house actually depend on
Sep 2026

Immich + Homepage

Deployed Immich for photo and video backup, plus Homepage as a single dashboard for every service — including a Security Checks panel fed by a script that reports host health and security posture every 5 minutes.

—Immich replaces ad-hoc phone-photo backups with something self-hosted
—Grafana and InfluxDB run alongside for metrics
—Dashboard secrets moved out of config files into protected env files
Sep 2026

Remote access + HTTPS

Remote access went live through Tailscale instead of a hand-built WireGuard setup: the server is a subnet router, so any of my devices reaches every service by its normal LAN address, with Pi-hole filtering applied remotely too. Caddy became the reverse proxy with friendly .lan names, and Vaultwarden (a self-hosted password manager) gets a real Let's Encrypt certificate through a Cloudflare DNS-01 challenge — with no public DNS record at all.

—Zero router ports forwarded — nothing is exposed to the public internet
—Cloudflare token scoped to DNS edits on one domain only, kept out of the logs
—Vaultwarden backed up automatically every night to a second location
Sep 2026

Hardening + monitoring

Turned on the Proxmox firewall with a default-deny inbound policy and per-container allow-lists, so each app only accepts traffic from the proxy and the dashboard. Uptime Kuma runs 15 uptime checks across the lab (plus Harakeh) and alerts my phone through Telegram; Diun sends a Telegram message when any app has a new release.

—Key-only SSH on the AI dev container
—Unused services disabled, host fully updated, first ZFS scrub run
—Every app updated with snapshots taken first as rollback points
Now

Automation + tools

The lab now hosts n8n (which runs my Telegram assistant), an always-on container with Claude Code and Codex that I control from my phone and keep in sync with my PC through Syncthing, a Samba drop share for moving files between devices, and Homebox for tracking what I own and where it's stored.

—AI agents get a scoped helper that can only check, start or stop approved containers
—A one-click shutdown button, restricted to the LAN and tailnet
—Each new service gets a firewall rule, a reverse-proxy name and an uptime check
Next

Second node

The second node — an older laptop — still needs its Proxmox install. Its planned role is a replication target for the main node's data, which matters because photos currently live on a single machine. The router also still isn't pointed at Pi-hole, so filtering covers only devices I've configured by hand or that connect over Tailscale.

—Next: zfs send/receive replication to the laptop node
—Next: router-wide DNS so every device in the house is covered
—Still on the list: a Minecraft server

Milestones

What broke

Tailscale broke DNS inside every container
Tailscale rewrote the host's resolver to its own MagicDNS address, and Proxmox copies the host's resolver into containers that don't set one — so Immich, Grafana and Homepage all lost name resolution. Fixed by pinning each container to Pi-hole and stopping Tailscale from managing the host's DNS.
A status script failed silently for days
The security-checks script worked when run by hand but failed under cron, because cron's PATH didn't include the Proxmox tools. The dashboard kept showing stale data with no error. One PATH line fixed it, and now I verify cron jobs under cron, not just in my shell.
A service raced the network at boot
The remote-control service for the AI dev container started before the network was ready, failed once, and never retried. It now waits for DNS to resolve before starting and restarts on failure.
Striped storage has no redundancy
hdd-pool stripes two drives for capacity, so losing either one loses everything on it. That's acceptable for replaceable media but not for photos — which is exactly why replicating to the second node is the next milestone.

From the repo

proxmox/storage-setup.sh
# fast tier — VM/container state
zpool create ssd-pool /dev/sda

# bulk tier — striped for capacity, no redundancy
zpool create hdd-pool /dev/sdb /dev/sdd
zfs set compression=lz4 hdd-pool

# cap ARC so it doesn't starve VMs
echo "options zfs zfs_arc_max=4294967296" \
  > /etc/modprobe.d/zfs.conf
HypervisorProxmox VE 9, LXC, Docker
StorageZFS — ssd-pool + hdd-pool
NetworkTailscale subnet router, 0 open ports
ProxyCaddy, Let's Encrypt via DNS-01
DNSPi-hole v6, ~4.4M domains blocked
AppsImmich, Vaultwarden, n8n, Homepage, Grafana, Homebox
MonitoringUptime Kuma, Diun → Telegram
Next project
Assistant
Open →