Network: How Traffic and DNS Work
Single subnet, DHCP on the router, two active Pi-hole DNS servers plus Google fallbacks, and one VIP that the internet talks to. Here’s how I have it set up.
One LAN, no VLANs
Everything is on one flat subnet. The router is the gateway and the only DHCP server. Every infrastructure device has a DHCP reservation (MAC → IP) so IPs don’t change. I don’t run VLANs or a managed switch; isolation is host-level (firewall, SSH, nginx allow/deny) rather than L2.
DHCP (router)
The MikroTik hEX runs the DHCP server on the LAN interface. In the DHCP network config I set:
- Subnet and gateway (router IP).
- DNS servers: two Pi-hole IPs (Host 1 VM first, then TrueNAS app) plus two Google DNS fallbacks. Pi-hole 3 (Host 2) is offline. Clients try each in order if one is down.
- Domain name (my internal domain, same as the public one for simplicity).
Lease time is short (e.g. 10 minutes) so that if a DNS node fails, clients refresh and pick another quickly. Tradeoff: a bit more DHCP traffic.
DNS: two active Pi-holes
I run two active Pi-hole instances (three were deployed; one went offline 2026-04-11):
- Pi-hole 1 — VM on Host 1 (same host as the Docker VM and nginx primary).
- Pi-hole 2 — TrueNAS Scale app on Host 3 (same IP as the NAS, different port).
- Pi-hole 3 — (offline — Host 2 decommissioned 2026-04-11)
They’re independent: same blocklists and similar config, but no automatic sync between them. I use the Pi-hole web UI (over HTTPS via nginx, LAN-only) to manage each. Two Google DNS fallbacks are also in the DHCP list.
How HTTPS reaches the services
- Router — Port forward: external 80 and 443 → internal VIP (the keepalived floating address). The router doesn’t know about “primary” or “secondary” nginx; it always sends traffic to the VIP. Currently only the primary nginx VM is active (secondary offline since Host 2 was decommissioned 2026-04-11); no automatic failover until a replacement is provisioned.
- nginx (primary) — Listens on the VIP (and its own fixed IP). One config file: all upstreams (Plex, Bitwarden, Mealie, etc.) and all
server_nameblocks (e.g.plex.detellem.com,bitwarden.detellem.com). TLS is terminated here; backends are HTTP. One wildcard cert covers all subdomains. - Backends — nginx proxies to the right host:port by
Hostheader. Plex is on Host 1 (Windows); Bitwarden, Mealie, and the rest are on the Docker VM on Host 1; Pi-hole admin is proxied to each Pi-hole’s IP/port.
So: Internet → router (NAT) → VIP → nginx → backend. No client ever talks directly to the Docker VM or Plex from the internet; only nginx does.
Certificates and domain
- Domain: I own a domain and use it for everything (same name internally and externally). The router’s public IP changes; I run a dynamic DNS script on two nodes (primary nginx VM and Pi-hole 1 VM) so the domain’s A record stays updated every 30 minutes via a systemd timer. Secondary nginx DDNS is disabled (Host 2 offline).
- Certificate: Let’s Encrypt wildcard (
*.detellem.com) so one cert covers all subdomains. Certbot on the primary nginx VM uses a DNS challenge (Google Cloud DNS API) so no need to open HTTP for validation. Cert sync to secondary is currently disabled (secondary nginx offline since 2026-04-11). - Hairpin NAT: When I’m on the LAN and hit
https://plex.detellem.com, the router sees that the destination is the VIP and does hairpin NAT so the response comes back correctly. So one URL works from inside and outside.
Summary
| Piece | What I use |
|---|---|
| Subnet | Single LAN, no VLANs |
| DHCP | MikroTik hEX, reservations for all infra |
| DNS | Two Pi-holes active (Host 1 VM, Host 3 app); Pi-hole 3 (Host 2) offline |
| Inbound | Router forwards 80/443 → VIP; keepalived holds VIP on primary nginx (secondary offline) |
| TLS | Wildcard Let’s Encrypt; certbot on primary; sync to secondary disabled (secondary offline) |
| DDNS | Script on primary nginx and Pi-hole 1; systemd timer; secondary nginx disabled |