Network: How Traffic and DNS Work
Single subnet, DHCP on the router, three Pi-hole DNS servers (automatically synced) plus a Google fallback, 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: three Pi-hole IPs (Docker VM on Desktop Host first, then NAS Host, then Proxy Host) plus one Google DNS fallback. 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: three Pi-holes (synced)
I run three Pi-hole instances:
- Pi-hole 1 — Docker container on the Docker VM on the Desktop Host (
network_mode: host). - Pi-hole 2 — Docker container on the NAS Host (same IP as the NAS, different port).
- Pi-hole 3 — Docker container on the Proxy Host (
network_mode: host; DNS on port 53, web admin on a non-standard port).
Nebula Sync automatically replicates blocklists, local DNS records, groups, and settings from the primary (Pi-hole 1) to both replicas every 15 minutes via the Pi-hole v6 REST API. So configuration changes only need to happen on the primary — they propagate automatically. A Google DNS fallback is also in the DHCP list for resilience if all three Pi-holes are down.
I use the Pi-hole web UI (over HTTPS via nginx, LAN-only) to manage the primary instance.
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. keepalived runs on both nodes: Proxy Host (preferred MASTER) and the nginx VM on the Desktop Host (BACKUP). If the MASTER’s nginx fails a health check, the VIP moves to the BACKUP automatically.
- nginx — Listens on the VIP (and each node’s fixed IP). Per-service config files under
conf.d/(e.g.plex.conf,vaultwarden.conf,homepage.conf) — not one monolithic file. All upstreams andserver_nameblocks are split by service. 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 the Desktop Host (Windows); Vaultwarden, Mealie, and the rest are on the Docker VM; 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 updater wired to keepalived — only the node currently holding the VIP updates the DNS A record (via
notify_master/notify_backupscripts). So the domain always points to my current public IP regardless of which nginx node is active. - Certificate: Let’s Encrypt wildcard (
*.detellem.com) so one cert covers all subdomains. Certbot on the config primary (nginx VM on Desktop Host) uses a DNS challenge (Google Cloud DNS API) so no need to open HTTP for validation. Cert sync to the Proxy Host is active (renew_hook + inotify-based sync from config primary). - 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 | Three Pi-holes (Desktop Host Docker VM, NAS Host, Proxy Host); Nebula Sync replication; Google fallback |
| Inbound | Router forwards 80/443 → VIP; keepalived HA — Proxy Host (MASTER) + nginx VM (BACKUP) |
| TLS | Wildcard Let’s Encrypt; certbot on config primary (nginx VM); sync to Proxy Host |
| DDNS | keepalived notify-triggered — only the VIP holder updates the A record |