A fully self-managed home lab built around a Lenovo Y50-70 media server, an Intel NUC7i7DNHE LLM server, and a RackNerd VPS acting as a secure WireGuard gateway. All services are containerised with Docker, exposed via Nginx with Let's Encrypt SSL, and accessible through the domain bheem.pw.
The home network runs behind an ASUS GT-AX11000 router, with the AT&T BGW320 gateway in IP Passthrough mode (acting as a modem only). Pi-hole provides network-wide DNS and ad blocking.
| Device | Specs | Role | IP |
|---|---|---|---|
| Lenovo Y50-70 | Core i7, 16GB RAM, 1TB HDD, GTX 960M | Media server + services | 192.168.50.132 (wired) |
| Intel NUC7i7DNHE | Core i7-8650U, 16GB RAM | Local LLM server | 192.168.1.178 (TODO: migrate) |
| RackNerd VPS | 2.5GB RAM, KVM, Ubuntu 24.04 | WireGuard gateway + Nginx reverse proxy | 108.174.61.246 |
bheemenp9s0) — runs through a wall jack to the panel, into an ASUS LAN port. WiFi (wlp8s0) remains as automatic fallback (higher route metric).f0:76:1c:14:2b:4e → reserved to 192.168.50.132 in ASUS DHCPd0:7e:35:0c:ec:de (fallback)sudo lvextend -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv
sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv
Wired connection preference: ethernet has a lower route metric (100) than WiFi (600), so the Y50 automatically prefers the wired link and falls back to WiFi only if the cable is unplugged.
bheemllmeno1sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
Internet (AT&T Fiber)
↓
AT&T BGW320 (192.168.1.254) — IP Passthrough mode, WiFi disabled
↓ (blue cable: BGW320 LAN → ASUS WAN)
ASUS GT-AX11000 (192.168.50.1) — main router, single NAT
│ Public WAN IP: 107.206.70.206 (dynamic)
│
├── LAN → Lenovo Y50-70 (192.168.50.132) — wired, media + services
│ ↕ WireGuard tunnel (10.0.0.1 ↔ 10.0.0.2)
│ RackNerd VPS (108.174.61.246) — WireGuard + Nginx
│
├── LAN → PoE injector → Luxul XAP-810 (floor 2 WAP, SSID: gypsybheem)
│
├── WiFi → asus-bheem (main SSID)
│
└── Intel NUC7i7DNHE (192.168.1.178) — LLM (TODO: migrate to ASUS)
| Network | Subnet | Gateway | Notes |
|---|---|---|---|
| AT&T BGW320 | 192.168.1.x |
192.168.1.254 |
Passthrough/modem only; admin access |
| ASUS (main) | 192.168.50.x |
192.168.50.1 |
All devices live here |
| Luxul AP (mgmt) | 192.168.0.10 |
— | AP management IP only |
| 2GIG Edge panel AP | 172.18.1.2 |
— | Alarm system — DO NOT TOUCH |
| WireGuard tunnel | 10.0.0.x |
— | VPS ↔ Y50 |
| SSID | Source | Network |
|---|---|---|
asus-bheem |
ASUS GT-AX11000 | 192.168.50.x |
gypsybheem |
Luxul XAP-810 (floor 2) | 192.168.50.x |
Gangstabheem |
— |
TODO (optional): unify
asus-bheemandgypsybheeminto a single SSID + password for seamless floor-to-floor roaming.
Configured at http://192.168.1.254 → Firewall → IP Passthrough:
PassthroughDHCPS-fixedThis hands the public IP directly to the ASUS, avoiding double NAT. The ASUS WAN IP should equal the public IP (107.206.70.206).
Reboot order matters: if power is cut to either device, reboot the BGW320 first (wait until fully up), then the ASUS. Otherwise passthrough can silently drop into double-NAT (symptom: traceroute shows a
192.168.1.254hop and outbound packets die).
Both radios turned off at 192.168.1.254 → Home Network → Wi-Fi → (2.4 GHz and 5 GHz pages) → Wi-Fi Operation: Off → Save. This removes spectrum interference and prevents devices from rejoining the old network. Re-enable the same way if ever needed (requires wired or upstream access to the gateway).
VPS (10.0.0.1) ↔ Y50 (10.0.0.2). All external traffic hits the VPS first, gets routed through the tunnel to the Y50. The Y50 connects outbound to the VPS, so the tunnel is unaffected by LAN IP/subnet changes.
VPS config (/etc/wireguard/wg0.conf):
[Interface]
PrivateKey = <vps_private_key>
Address = 10.0.0.1/24
ListenPort = 51820
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey = <y50_public_key>
AllowedIPs = 10.0.0.2/32
Y50 config (/etc/wireguard/wg0.conf):
[Interface]
PrivateKey = <y50_private_key>
Address = 10.0.0.2/24
[Peer]
PublicKey = <vps_public_key>
Endpoint = 108.174.61.246:51820
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25
Pi-hole runs on the Y50 (192.168.50.132) in Docker (host network mode) and serves DNS + ad blocking for the whole network.
http://192.168.50.132/admin192.168.50.132, Server 2: 8.8.8.8192.168.50.132, 8.8.8.8true,192.168.50.0/24,192.168.50.1 — so client hostnames resolveUbuntu's systemd-resolved grabs port 53 and conflicts with Pi-hole. If Pi-hole shows DNS failure / dig @192.168.50.132 times out:
sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolved
sudo rm /etc/resolv.conf
echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf
cd ~/pihole && docker compose restart
dig google.com @192.168.50.132 # verify
All exposed via Nginx on the VPS with Let's Encrypt SSL. Each has its own config in /etc/nginx/sites-available/. These are unaffected by LAN changes — Nginx proxies to the WireGuard tunnel IP (10.0.0.2), not the LAN IP.
| Service | URL | Port |
|---|---|---|
| Dashy | https://dashy.bheem.pw |
4000 |
| Vaultwarden | https://vaultwarden.bheem.pw |
8888 |
| BentoPDF | https://bentopdf.bheem.pw |
8889 |
| Tandoor | https://tandoor.bheem.pw |
8891 |
| BabyBuddy | https://babybuddy.bheem.pw |
8090 |
| Seerr | https://seerr.bheem.pw |
5055 |
| Wiki.js | https://wiki.bheem.pw |
3001 |
server {
listen 80;
server_name service.bheem.pw;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name service.bheem.pw;
ssl_certificate /etc/letsencrypt/live/service.bheem.pw/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/service.bheem.pw/privkey.pem;
location / {
proxy_pass http://10.0.0.2:<port>;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
All services run as separate Docker Compose stacks under ~/media/ (arr stack) or their own home directories. Docker version: 29.4.1. Local access uses 192.168.50.132:<port>.
~/media/)| Service | Port | Purpose |
|---|---|---|
| qBittorrent | 8080 | Download client (behind Gluetun VPN kill switch) |
| Gluetun | — | NordVPN OpenVPN kill switch for qBittorrent |
| Prowlarr | 9696 | Indexer manager |
| FlareSolverr | 8191 | Cloudflare bypass for Prowlarr indexers |
| Radarr | 7878 | Movie automation |
| Sonarr | 8989 | TV show automation |
| Bazarr | 6767 | Automatic subtitle downloads |
| Profilarr | 6868 | Quality profile sync for Radarr/Sonarr |
| Jellyfin | 8096 | Media streaming (VAAPI hardware acceleration) |
| Tdarr | 8265/8266 | Background HEVC transcoding |
| Seerr | 5055 | Media request manager |
| Watchtower | — | Auto container updates (daily at 4am) |
Note on the arr stack and the IP change: internal connections use
localhost/ container names / Gluetun, so the LAN IP change required no edits. qBittorrent (via Gluetun) and Jellyfin work unchanged. Only the local access URL for Jellyfin changed (TV app repointed to192.168.50.132:8096).
| Service | Directory | Port | Notes |
|---|---|---|---|
| Vaultwarden | ~/vaultwarden/ |
8888/3012 | Argon2 admin token via .env, signups disabled |
| BentoPDF | ~/bentopdf/ |
8889 | Privacy-first PDF toolbox |
| BabyBuddy | ~/babybuddy/ |
8090 | Baby tracking |
| Tandoor | ~/tandoor/ |
8891 | Recipe manager with PostgreSQL backend |
| Dashy | ~/dashy/ |
4000 | Home lab dashboard |
| Wiki.js | ~/wikijs/ |
3001 | Documentation wiki (PostgreSQL backend) |
| Pi-hole | ~/pihole/ |
80/53 | Network-wide DNS + ad blocking (host network mode) |
Gluetun + qBittorrent kill switch:
qbittorrent:
network_mode: service:gluetun
qBittorrent has no independent network access — all traffic routes through Gluetun. If VPN drops, downloads stop immediately. Unaffected by LAN changes (Gluetun connects outbound).
Pi-hole v6 password env var:
FTLCONF_webserver_api_password: "yourpassword"
Vaultwarden Argon2 token — $ signs in the hash must be escaped as $$ in Docker Compose environment blocks. Use an .env file to avoid this.
Tandoor requires a custom Nginx config volume mount. Override /opt/recipes/nginx/conf.d/Recipes.conf to use proxy_pass http://localhost:8080.
Jellyfin hardware acceleration — VAAPI configured with /dev/dri:/dev/dri device mount. Set in Dashboard → Playback → Transcoding → VAAPI → /dev/dri/renderD128.
Tdarr flow — Arr Stack HEVC Safe Flow: skips HEVC/AV1/HDR/DV, transcodes H264→HEVC MKV, validates output size (35–110%), runs nightly 02:00–06:00.
| Service | Port | Purpose |
|---|---|---|
| Ollama | 11434 | Local LLM inference |
| Open WebUI | 3000 | Browser chat interface |
llama3.1:8b — general assistantqwen2.5-coder:7b — code assistantsudo mkdir -p /etc/systemd/system/ollama.service.d
# override.conf:
[Service]
Environment="OLLAMA_HOST=0.0.0.0"
sudo systemctl daemon-reload && sudo systemctl restart ollama
services:
open-webui:
container_name: open-webui
image: ghcr.io/open-webui/open-webui:main
environment:
- OLLAMA_BASE_URL=http://172.17.0.1:11434
- WEBUI_SECRET_KEY=changethis123
- TZ=America/Chicago
volumes:
- ./data:/app/backend/data
ports:
- 3000:8080
restart: unless-stopped
172.17.0.1 is the Docker bridge gateway.
Domain bheem.pw managed via Spaceship registrar. A records point to VPS IP 108.174.61.246:
| Type | Name | Value |
|---|---|---|
| A | @ |
108.174.61.246 |
| A | * |
108.174.61.246 |
The wildcard * record means all subdomains resolve to the VPS. Unaffected by LAN changes — external traffic always targets the VPS.
docker-compose.yml on the Y50docker compose up -dcertbot --nginx -d newservice.bheem.pw/etc/nginx/sites-available/ (proxy to http://10.0.0.2:<port>)ln -s /etc/nginx/sites-available/newservice /etc/nginx/sites-enabled/nginx -t && systemctl reload nginx~/dashy/conf.yml and restart Dashy# Containers on Y50
docker ps
# WireGuard tunnel status
sudo wg show
# Restart a service
cd ~/servicename && docker compose restart
# Service logs
docker logs containername --tail 50
# Disk / resources
df -h /
htop
# SSH
ssh bheem@192.168.50.132 # Y50 (new IP)
ssh bheemllm@192.168.1.178 # NUC (until migrated)
ssh root@108.174.61.246 # VPS
# Network diagnosis (see incident doc)
ip route | grep default # which gateway?
ip addr show enp9s0 # wired IP
nslookup google.com # check Pi-hole is answering
| Symptom | Likely cause | Check / fix |
|---|---|---|
| Device can't reach another on "same" network | Different subnets | Compare ip route \| grep default on both; TTL 62/63 in ping = crossed routers |
| No internet but router shows connected | Passthrough dropped to double-NAT | traceroute 8.8.8.8 — if hop shows 192.168.1.254, reboot BGW320 then ASUS |
| Pi-hole DNS failure | systemd-resolved on port 53 | See Pi-hole section |
| Pi-hole only shows router as client | Conditional forwarding off | Set true,192.168.50.0/24,192.168.50.1 |
| Server lost after SSID change | WiFi server orphaned | Prefer wired ethernet for servers |
See the Incident: Dual-Subnet Confusion page for the full postmortem and diagnosis playbook.
TODO — not yet implemented. Planned: nightly Vaultwarden backup via cron, plus Tandoor PostgreSQL and BabyBuddy data.
#!/bin/bash
BACKUP_DIR=~/backups/vaultwarden
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR
cp -r ~/vaultwarden/data $BACKUP_DIR/vaultwarden_$DATE
ls -dt $BACKUP_DIR/* | tail -n +8 | xargs rm -rf
Networking cleanup (in progress):
asus-bheem or wired, then DHCP reservation)192.168.1.90 references (likely none — uses localhost/containers)asus-bheem + gypsybheem SSIDs for seamless roaming (optional)192.168.50.x for easier adminOriginal future plans:
172.18.1.2) + its own AP — managed by HomePro; leave alone