Run one local DNS server, give every service a hostname under a real domain you own, and answer that domain locally while the public internet sees nothing — split-horizon. This single pattern eliminates the three chronic homelab annoyances at once: bookmarks full of 192.168.1.20:8096, TLS warnings on internal services, and the hairpin-NAT mystery where a hostname works from a coffee shop but dies on your own sofa.
Use a real domain, and retire .local
A domain costs $3–12/year and it's the load-bearing decision. .local belongs to mDNS (RFC 6762) and resolvers handle it inconsistently; .lan and .home are squatted conventions; .internal was formally reserved for private use by ICANN in 2024, but no public CA will ever issue certificates for it. A real domain is the only path to Let's Encrypt certs on internal hosts — the DNS-01 wildcard setup depends on it. Buy one, then dedicate a subdomain to the lab: *.home.example.com.
Split-horizon in one line
Pi-hole is dnsmasq underneath, so the entire internal zone is a wildcard:
# /etc/dnsmasq.d/99-split.conf
address=/home.example.com/192.168.1.10
(On Pi-hole v6, set misc.etc_dnsmasq_d: true so custom dnsmasq files load.) Every *.home.example.com now resolves to your reverse proxy at .10, which routes by Host header — adding a service becomes one block in Caddy or Traefik and zero DNS work, forever. AdGuard Home does the identical thing through its DNS-rewrites UI if you're on the other side of that comparison.
On public DNS, publish nothing for home.example.com and your services are unresolvable from outside — invisible without a VPN. The variant worth knowing: publish AAAA/A records pointing at Tailscale addresses, and the same hostnames work from anywhere while remaining useless to strangers.
Records and TTLs worth knowing
| Record | Homelab use | TTL guidance |
|---|---|---|
| A / AAAA | The proxy; anything you SSH into | 300s while building, 3600+ once stable |
| CNAME | Aliases pointing at the proxy | Inherits target's; not valid at zone apex |
| TXT | ACME DNS-01 challenges | Managed by your ACME client; hands off |
| CAA | Restrict which CA can issue | 86400; one-line insurance |
| PTR | Reverse lookups; readable logs | Local only; dnsmasq serves them free |
The only genuinely dynamic record is your home's public IP if you expose anything directly: run a DDNS updater (your router's, or a cron job against the DNS API) with a 120-second TTL. Everything internal can sit at long TTLs because the wildcard means you almost never touch individual records.
The classic resolution bugs, diagnosed
Works on mobile data, dead on Wi-Fi. Hairpin NAT: a LAN client resolving your public hostname to your WAN IP, and the router refusing to loop the connection back inside. Split-horizon is the fix — internal clients get the internal answer and never touch the WAN interface.
Pi-hole works, except on the phone. Android's Private DNS (DoT) talks straight to Google or Cloudflare, skipping your resolver — internal names fail and ads return. Turn Private DNS off for the home network, or point it at your own DoT-capable resolver.
One device ignores your DNS entirely. Chromecasts and a lot of IoT hardcode 8.8.8.8. The firewall fix is a NAT redirect of all outbound port-53 traffic to your resolver; encrypted DoH bypass is harder to police, which is one more argument for keeping that junk on its own VLAN.
Containers can't resolve internal names. Docker copies the host's /etc/resolv.conf, and on systemd-resolved hosts that's the 127.0.0.53 stub — unreachable from inside a container, so Docker silently falls back to 8.8.8.8 and your split-horizon names vanish. Fix it explicitly: dns: [192.168.1.53] per service, or "dns" in daemon.json.
The record you just created doesn't resolve. You queried it before creating it, and the NXDOMAIN is sitting in negative cache for the zone's SOA-derived TTL. Flush (pihole restartdns, or your OS equivalent) instead of debugging a working record.
DNS is your real single point of failure — run two
When the resolver reboots, the household verdict is "the internet is down", and every service you host goes with it. Run a second instance on physically separate hardware — a Pi Zero 2 W at ~$15 is entirely adequate for DNS — and hand out both resolvers via DHCP. Keep the wildcard config and blocklists aligned with a nightly Teleporter export/import or one of the sync tools; what matters is that both instances carry the local zone, or failover will "work" while every internal hostname breaks. Don't put both on the same Docker host, which is the most common way people build a redundant single point of failure. Reference for everything Pi-hole-specific: docs.pi-hole.net.
What I'd do
One real domain. Wildcard address= line sending *.home.example.com to the reverse proxy. DNS-01 wildcard certificate on that proxy so every internal service gets a padlock. TTLs at 300 until the layout stops changing, then 3600. Two resolvers on separate metal, both carrying the local zone. Total cost: about $12/year and an hour — against which you never type an IP address, dodge a certificate warning, or debug hairpin NAT again.