Here’s the problem: I have ideas. Lots of them. Web apps to build, IoT devices to test, services to self-host, websites to launch. Most of them won’t make money. Some might. I don’t know which until I build them.
But building things needs infrastructure. Servers, networking, storage, CI/CD pipelines, public-facing endpoints. The traditional path — buy hardware, pay for cloud, subscribe to SaaS tools — assumes you have budget. I don’t. Not for experiments that might go nowhere.
So I built a homelab for near-zero cost. One Dell OptiPlex 7090 running Hyper-V, three physical NICs, a free Oracle Cloud instance, and Tailscale to tie them together. This single box runs my router (pfSense), my Docker host (Ubuntu VM), my IoT hub (Home Assistant), and eight terabytes of media storage. Total hardware cost: whatever a used OptiPlex costs on eBay. Monthly cloud bill: $0.00.
Every product on this site, every web app I’ve shipped, every ESPHome device I’ve flashed — it all started on this setup. The homelab isn’t the goal. It’s the enabler. It’s what lets me try things without financial risk. This guide shows you how I built it, why each decision was made, and — because cheap hardware fails — how Veeam brings it all back when it goes down.
Architecture Overview
Here’s the high-level topology. One physical box, one cloud instance, and Tailscale tying them together:
INTERNET
│
▼
┌──────────────────────────────┐
│ ORACLE CLOUD │
│ (ARM64, free tier) │
│ │
│ Nginx Proxy Manager │ ← TLS termination, Lets Encrypt
│ Authentik SSO │ ← Forward-auth for all apps
│ Drone CI │ ← GitHub-integrated CI/CD
│ App containers (12+) │
└──────────────┬───────────────┘
│
Tailscale mesh VPN
│
┌──────────────┴───────────────┐
│ DELL OPTIPLEX 7090 │
│ (Windows Server 2019) │
│ Hyper-V Host │
│ │
│ ┌────────────────────────┐ │
│ │ pfSense VM (router) │ │
│ │ WAN, LAN, VLAN routing │ │
│ └────────────────────────┘ │
│ ┌────────────────────────┐ │
│ │ Ubuntu Server VM │ │
│ │ Docker: Jellyfin, │ │
│ │ Nextcloud, Collabora │ │
│ └────────────────────────┘ │
│ ┌────────────────────────┐ │
│ │ Home Assistant VM │ │
│ │ IoT & automations │ │
│ └────────────────────────┘ │
└──────────────────────────────┘
Three physical NICs handle traffic separation:
| NIC | Purpose | Connected To |
|---|---|---|
| Intel I219-LM (onboard) | WAN uplink | ISP modem → pfSense VM |
| Intel 82580 #1 | Main LAN (192.168.2.0/24) | Switch → WiFi APs, wired clients |
| Intel 82580 #2 | VLAN trunk | Server VLAN, Camera VLAN, Management VLAN |
Why One Box?
Not because it’s elegant. Because it’s cheap.
I didn’t have budget for a rack server, a managed switch, a dedicated pfSense appliance, a NAS, and a separate dev box. Even used enterprise gear adds up. The OptiPlex was already there — it started as a file server. Everything else accreted onto it because each addition cost nothing: Hyper-V is built into Windows Server, pfSense is free, Ubuntu is free, Docker is free, Tailscale is free, Oracle Cloud has an always-free tier.
When an idea fails (and most do), I’ve lost nothing but time. When an idea works, I’ve validated it without burning cash on infrastructure. That’s the difference between “I wonder if this would work” and actually finding out.
The tradeoffs:
| Pro | Con |
|---|---|
| Near-zero cost — one used desktop PC | Single point of failure for everything |
| Free software stack (Hyper-V, pfSense, Ubuntu, Docker, Tailscale, Veeam CE) | Consumer hardware — no ECC RAM, no redundant PSU |
| Try ideas without financial risk | If the host reboots, everything goes down including the router |
| Virtual networking between VMs is wire-speed, no switch required | vSwitch and VLAN config has a learning curve |
For experimenting on a budget, the tradeoffs work. But you need a backup plan. More on Veeam below.
Hyper-V Setup: The Foundation
Host Hardware
| Component | Spec |
|---|---|
| Model | Dell OptiPlex 7090 |
| CPU | Intel Core i5-10505 (6 cores, 12 threads @ 3.2 GHz) |
| RAM | 32 GB DDR4 |
| OS | Windows Server 2019 (build 17763) |
| Storage | 1 TB NVMe (OS + VMs), 8 TB HDD (media) |
| NICs | 1x Intel I219-LM, 2x Intel 82580 Gigabit |
The OptiPlex 7090 is a business desktop, not a server. No ECC memory, no redundant power supplies, no iDRAC. But it’s quiet, sips power compared to a rack server, and the 10th-gen i5 handles the workload comfortably. The 32 GB of RAM is the real constraint — split across pfSense (1 GB), Ubuntu (8 GB), Home Assistant (2 GB), and Windows itself, there’s not a lot of headroom. Plan your VM memory allocations carefully.
Hyper-V Virtual Switch Layout
This is the most important configuration decision. You need three virtual switches, each bound to a specific physical NIC:
Virtual Switch "WAN" → Intel I219-LM → ISP modem
Virtual Switch "LAN" → Intel 82580 #1 → Physical switch (192.168.2.0/24)
Virtual Switch "Trunk" → Intel 82580 #2 → VLAN trunk for servers/cameras/mgmt
Critical: The WAN switch must be an External switch with “Allow management operating system to share this network adapter” UNCHECKED. This dedicates the NIC to pfSense. If you leave it checked, your Hyper-V host gets a public IP directly on the internet — not what you want.
For the LAN and Trunk switches, use External type with management OS sharing enabled. This lets the Hyper-V host itself have IPs on those networks (useful for file shares, RDP access, and management).
Virtualizing pfSense: The Tricky Part
Most homelab guides will tell you to run pfSense on dedicated hardware. I get it — if your hypervisor goes down, your internet goes down, and you can’t Google how to fix it. Fair point.
But I’ve been running pfSense as a Hyper-V VM for over two years without a pfSense-caused outage. Here’s what makes it work:
VM Configuration
| Setting | Value | Why |
|---|---|---|
| vCPUs | 1 | pfSense is lightweight. A single vCPU handles gigabit routing fine for home use |
| RAM | 1 GB (static) | Don’t use dynamic memory — pfSense/FreeBSD doesn’t play well with it |
| Network Adapter 1 | WAN vSwitch | Gets public IP from ISP via DHCP |
| Network Adapter 2 | LAN vSwitch | 192.168.2.1 — DHCP server, DNS resolver |
| Network Adapter 3 | Trunk vSwitch | VLAN-tagged interfaces for servers, cameras, management |
| Boot order | Start before other VMs | Other VMs depend on pfSense for DHCP and DNS |
VLAN Setup on pfSense
From pfSense, I create VLAN interfaces on the trunk adapter:
VLAN 5 → Server VLAN (192.168.5.0/24) — Hyper-V host, Ubuntu VM
VLAN 4 → Camera VLAN (192.168.4.0/24) — IP cameras, isolated from internet
VLAN 6 → Management (192.168.6.0/24) — AP controller, switch management
Each VLAN gets its own DHCP server and firewall rules on pfSense. The key rule: main LAN can reach server VLAN, but server VLAN cannot initiate connections to main LAN. Cameras have no internet access at all — they can only talk to the NVR.
Hyper-V Host Networking
The Hyper-V host itself gets IPs on two VLANs via the Trunk switch:
LAN adapter: 192.168.2.x (main LAN, for RDP access)
Server VLAN: 192.168.5.x (SMB file shares for Ubuntu VM)
Camera VLAN: 192.168.4.x (reach cameras for configuration)
Management VLAN: 192.168.6.x (AP controller)
This means the host can reach everything, but clients on the main LAN can’t directly reach the server VLAN (unless you add firewall rules). The Ubuntu VM accesses the host’s SMB shares via the server VLAN IP — no need to hairpin through the router.
Docker Services on Ubuntu VM
The Ubuntu Server VM is the workhorse. It’s a standard Ubuntu install with Docker and Portainer for management. Services that need lots of storage or x86-only Docker images live here:
| Service | Why On-Prem |
|---|---|
| Jellyfin (media server) | 8 TB of media on local disk. Cloud storage would cost a fortune |
| Nextcloud (file sync) | Large files, local access speed |
| Collabora Online | Document editing integrated with Nextcloud |
| Copyparty | Quick file sharing without uploading to cloud |
SMB Mount at Boot
The Ubuntu VM mounts the host’s media drive via CIFS. The trick is ensuring the mount happens before Docker starts — otherwise Jellyfin boots with an empty media library. Systemd to the rescue:
# /etc/systemd/system/ensure-media-mount.service
[Unit]
Description=Wait for SMB media mount before Docker
Before=docker.service
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/wait-for-mount.sh
TimeoutSec=150
[Install]
WantedBy=multi-user.target
The script retries the mount 30 times with 5-second delays. If the host is still booting, it waits. Docker only starts once the mount is confirmed.
Oracle Cloud: The Internet-Facing Edge
Not everything can live on-prem. Public-facing web apps need a public IP, and my home ISP doesn’t provide a static one. Enter Oracle Cloud’s Always Free Tier — an ARM64 instance with 4 OCPUs and 24 GB of RAM. Zero cost.
Services running on Oracle:
| Service | Role |
|---|---|
| Nginx Proxy Manager | TLS termination, Lets Encrypt auto-renewal, domain-based routing. The single entry point for all internet traffic |
| Authentik SSO | Identity provider. Handles forward-auth for every app — users log in once, Authentik validates every request |
| Drone CI | CI/CD pipelines triggered by GitHub pushes. Builds Docker images and deploys to both Oracle and on-prem |
| Immich | Photo backup and management (migrated from on-prem) |
| Minecraft Server | Because every homelab needs one |
Pattern: services that need public access → Oracle. Services that need bulk storage or x86-only images → on-prem Ubuntu VM.
Tailscale: The Invisible Backbone
Tailscale is what makes the hybrid architecture work. It’s a WireGuard-based mesh VPN that connects every device without opening firewall ports. No port forwarding, no dynamic DNS, no complex VPN config.
Every node gets a stable 100.x.x.x IP:
Oracle Cloud instance → 100.x.x.x
Hyper-V host → 100.x.x.x
Ubuntu Server VM → 100.x.x.x
Home Assistant VM → 100.x.x.x
Dev laptop → 100.x.x.x
Mobile phones → 100.x.x.x
How Tailscale Routes Traffic
Nginx Proxy Manager on Oracle receives all internet traffic. For apps running on Oracle itself, NPM routes to Docker containers by hostname (Docker’s internal DNS). For apps on the on-prem Ubuntu VM, NPM forwards to the VM’s Tailscale IP:
Internet → NPM (Oracle) → Tailscale → Ubuntu VM (on-prem) → Docker container
This is the killer feature. NPM doesn’t care where the backend lives — cloud or on-prem, it all looks local on the Tailscale mesh. The Ubuntu VM doesn’t need a public IP or an open port on the router.
SSH Hardening
SSH on the Oracle instance is restricted to Tailscale IPs only. Connections from the public internet are rejected before authentication. Even if someone gets your private key, they can’t use it unless they’re on your Tailscale network.
Home Assistant: IoT Without the Cloud
Home Assistant runs as its own Hyper-V VM (1 vCPU, 2 GB RAM). It handles:
- TP-Link Omada controller integration — monitors WiFi AP status, connected clients
- Reolink doorbell camera — motion alerts, snapshot automation
- ESPHome devices — Shelly Pro 1PM controlling a hot water diverter (solar export surplus → hot water element), ESP32 zone controllers for HVAC
The hot water automation is worth its own article. In short: when solar exports exceed 3,600W, a Shelly relay diverts power to the hot water element instead of selling it back to the grid at a fraction of the cost. Home Assistant reads the export data, checks a threshold, and toggles the relay — all locally, no cloud dependency.
Disaster Recovery: Veeam and the “Oh No” Plan
This is the section I hope you never need. But if you’re running everything on one box, you’d better have it.
What Goes Wrong When the Host Dies
If the OptiPlex hardware fails, here’s what stops working simultaneously:
| Service | Impact |
|---|---|
| pfSense VM | All internet access — WiFi, wired, everything. You can’t even Google the fix |
| Ubuntu Server VM | Jellyfin (media), Nextcloud (files), Collabora (docs) — all offline |
| Home Assistant VM | All automations stop. Hot water diverter, cameras, HVAC zone control — dead |
| SMB shares | No access to media library from any device |
The internet goes down with the router because the router is a VM. That’s the downside of virtualized pfSense. You need a backup internet path (phone hotspot) just to troubleshoot.
Veeam Backup Strategy
Veeam Backup & Replication (Community Edition — free for up to 10 workloads) handles the heavy lifting. Here’s the setup:
| What | How | Schedule |
|---|---|---|
| pfSense VM | Veeam Hyper-V backup (entire VM) | Weekly, config export weekly |
| Ubuntu Server VM | Veeam Hyper-V backup (entire VM) | Daily incremental, weekly synthetic full |
| Home Assistant VM | Veeam Hyper-V backup (entire VM) | Daily |
| Docker volumes (Oracle) | SSH tar-pipe to local storage | Weekly (manual script) |
| pfSense config | Auto Config Backup (pfSense built-in) | On every config change |
Veeam stores backups on an external USB drive attached to the Hyper-V host. If the host itself dies, plug that drive into any Windows machine with Veeam installed and restore.
Restore Process (From Cold Iron)
1. Replace/repair hardware (or grab any spare PC)
2. Install Windows Server 2019 + Hyper-V role
3. Install Veeam Backup & Replication Community Edition
4. Connect USB backup drive
5. Import backups → Restore VMs in order:
a. pfSense first (you need internet!)
b. Ubuntu Server
c. Home Assistant
6. Boot pfSense → verify WAN gets IP → verify LAN DHCP works
7. Boot Ubuntu → verify Docker services start → verify SMB mount
8. Boot Home Assistant → verify automations fire
9. Test external access: hit public URLs, verify NPM routes correctly
Expected downtime: 2–4 hours if you have spare hardware ready. 24+ hours if you need to order parts.
What Veeam Doesn’t Cover
Be honest about the gaps:
- Oracle Cloud Docker volumes — Veeam can’t back these up directly. I use a custom script that SSHs into Oracle, pipes Docker volumes through
tar, and saves them locally. It’s manual and weekly — improve this if your cloud services are critical - Nginx Proxy Manager config — stored in a SQLite database inside a Docker volume. Covered by the Docker volume backup, but a separate NPM config export would be faster to restore
- Physical host hardware — if the OptiPlex motherboard dies, you’re buying or scavenging a replacement. Keep a spare NIC handy (the Intel 82580 is a PCIe card, the I219-LM is onboard)
Testing Your Backups
A backup you haven’t tested isn’t a backup — it’s a hope. At minimum:
- Once a quarter, restore a VM to a different Hyper-V host (even a laptop with Hyper-V enabled)
- Verify the restored VM boots and services start
- Test that pfSense gets a WAN IP after restore (NIC MAC change can confuse some ISPs)
- Document any quirks you discover (MAC address re-binding, DHCP lease renewal, Tailscale re-auth)
Lessons Learned
1. Free Infrastructure Changes What You Can Afford to Try
When infrastructure costs money, every experiment is a financial decision. “Will this app make enough to cover the hosting?” is a question that kills ideas. When infrastructure is free, the only cost is time. You can build the thing, launch it, and then decide if it’s worth upgrading.
Every product on this site started here. Some failed. Some worked. None of them cost me a dollar in infrastructure before I knew they had legs.
2. Document Before You Need It
I mapped the topology because I was tired of guessing which service ran where. Now I have a single reference document. When something breaks at 11 PM, I’m not hunting through SSH sessions trying to remember which of three hosts runs Authentik.
3. Virtualized pfSense Is Fine — If You Plan for Failure
Two years, zero pfSense VM crashes. The key is static memory allocation (FreeBSD hates dynamic memory), dedicated NICs via vSwitch, and automatic config backups. Have a backup router plan — even if it’s just “turn on phone hotspot while I fix the server.”
4. Tailscale Changes Everything
Before Tailscale, connecting on-prem to cloud meant VPN servers, port forwarding, dynamic DNS, and fragile config. After Tailscale: install the agent, everything gets a stable IP, done. The Oracle-to-Ubuntu link over Tailscale has been rock-solid. Tailscale’s free tier covers 100 devices — more than enough.
5. Consumer Hardware Has Limits
32 GB of RAM sounds like a lot until you split it across four VMs and Windows. The i5-10505 is surprisingly capable — I’ve never hit CPU limits — but RAM is the bottleneck. If I were building fresh with a small budget, I’d prioritize RAM over CPU. 64 GB would be comfortable.
6. Oracle Free Tier Is Underrated
4 OCPUs, 24 GB RAM, ARM64 — zero cost, no credit card tricks, no “expires after 12 months.” It’s been running for over a year without issues. The only limitation is ARM64 architecture, which means some Docker images need rebuilding. Worth it.
Is This Setup Right for You?
This approach works well if:
- You want to experiment without spending money on infrastructure
- You’re comfortable with Hyper-V networking (vSwitches, VLANs)
- You accept the single-point-of-failure tradeoff in exchange for zero cost
- You have a tested backup and restore plan (Veeam CE, see above)
- You’re building things where the cost of not experimenting is higher than the risk of downtime
It’s probably not right if:
- Internet uptime is critical — keep pfSense on dedicated hardware
- You’re running services other people depend on (family, small business)
- You have budget for proper infrastructure (in which case, skip the compromises)
Resources
- Veeam Backup & Replication Community Edition — free for up to 10 workloads
- Tailscale — free for personal use, up to 100 devices
- Oracle Cloud Always Free Tier — ARM64 Ampere instances, 4 OCPU / 24 GB RAM
- pfSense — open-source router/firewall
- Nginx Proxy Manager — simple reverse proxy with Lets Encrypt
- Authentik — open-source SSO and identity provider
This article documents my personal homelab setup. Specific IP addresses, hostnames, and credentials have been redacted. Your network, your rules — adapt these patterns to your own hardware and requirements.
Leave a Reply