Tech Digest

Guide

DNS, subdomains and split horizon for self-hosters

Naming is the decision you make once and live with for years. Get it right and certificates, single sign-on and remote access all fall into place. Get it wrong and every one of them fights you.

Last reviewed

What domain should you use for self-hosted services on a home network?

Use subdomains of a real domain you own and pay for, such as jellyfin.home.example.com. Do not use .local, which RFC 6762 reserves for multicast DNS and which behaves inconsistently across operating systems, and do not invent a TLD like .lan or .box, because you can never get a publicly trusted certificate for a name you do not own and the string may be delegated as a real gTLD later. A domain costs roughly 10 to 15 dollars a year and unlocks trusted TLS on internal-only names through the DNS-01 challenge.

Almost every difficult problem in a homelab traces back to a naming decision made in the first week. Certificates are hard because the name is not real. Single sign-on is hard because the redirect URI changed when you moved a service. Remote access is hard because the hostname that works at home does not exist anywhere else. Fix the names and a surprising amount of the rest stops being a problem.

Buy a domain. It is the best twelve dollars in self-hosting.#

A .com or a cheap alternative TLD runs roughly 10 to 15 dollars a year. Here is what that actually buys, none of which is available at any price without it:

  • Publicly trusted certificates on internal-only names. With the DNS-01 challenge you prove control by publishing a TXT record, so nas.home.example.com can resolve to 192.168.1.20 and still get a real certificate from Let's Encrypt. No warning pages, no private CA to install on a TV.
  • Secure contexts. Passkeys, WebAuthn, service workers and the clipboard API only work over HTTPS with a trusted certificate. Without a real name, Vaultwarden passkeys and offline-capable web apps simply do not function.
  • Stable identity. OIDC redirect URIs, webhook endpoints and mobile app server addresses all embed the hostname. Change the naming scheme later and you re-enrol every client, including the Immich phone app and every Home Assistant companion install.
  • Independence from your ISP. The name survives a router replacement, a move, an address change and a switch to a different access technology.

The alternatives all cost more in time than the domain costs in money. That is the whole argument.

Why not .local, .lan or a TLD you made up#

.local is not yours. RFC 6762 defines it as a special domain with special semantics: any fully qualified name ending in .local. is link-local, and queries are supposed to go to multicast address 224.0.0.251 rather than to a DNS server. macOS, iOS and any Linux box running Avahi follow that and never ask your resolver at all. Windows and Android behave differently. So you end up with a name that resolves on the desktop, fails on the iPhone, and produces no error that hints at why. This is not a bug you can configure away; it is two protocols claiming the same namespace.

.lan, .box, .home and friends are squatting. They work today because nobody has delegated them. Three things follow. You will never get a publicly trusted certificate, because no CA can validate a name that has no owner. Browsers treat unknown single-word and unfamiliar-TLD hosts inconsistently, sometimes as search terms. And the string can become real: .dev was the fashionable fake local suffix for years, then it was delegated as a genuine gTLD with HSTS preloaded across the entire TLD, and every browser started forcing HTTPS on names that had never had a certificate. Removal from that list takes months and ships on browser release trains.

If you genuinely will not buy a domain, the two defensible non-routable choices are home.arpa, designated for residential home networks by RFC 8375, and .internal, the string ICANN identified for private-use applications. Both are reserved, so neither will be sold out from under you. Both still leave you without trusted certificates, which means running a private CA. Caddy will do that for you, minting its own local root, and you then install that root on every laptop, phone, tablet and set-top box in the house, and again on every guest device. That is the real price.

The scheme that works#

Pick one and be boring about it:

jellyfin.example.com          public, reachable from the internet
photos.example.com            public
ha.home.example.com           internal only
nas.home.example.com          internal only
proxmox.home.example.com      internal only

One label under a home. (or int., or lan.) delimiter for internal services, bare subdomains for anything public. The delimiter matters because it lets you point a single wildcard at your proxy for everything internal, and because a glance at a hostname tells you which side it lives on.

Wildcard records, and their two rules#

; Public zone at your registrar or DNS provider
example.com.            300  IN  A      203.0.113.42
*.example.com.          300  IN  CNAME  example.com.

; Everything internal points at the reverse proxy on the LAN
*.home.example.com.     300  IN  A      192.168.1.20

Two rules people learn the hard way:

  1. A wildcard covers exactly one label. *.example.com answers for photos.example.com and does not answer for dev.photos.example.com. This is also true of wildcard certificates, which is why the internal delimiter is worth its own explicit wildcard.
  2. An explicit record always wins. If nas.home.example.com exists as its own A record, the wildcard is never consulted for that name. That is useful, and it also means a stale explicit record silently overrides the wildcard you just fixed.

The wildcard is what makes adding a service a one-line change to your proxy (or to Reverse proxy generator) instead of a DNS change plus a certificate issuance. Combined with a wildcard certificate from DNS-01, new hostnames cost nothing, and you stop worrying about the 50 certificates per registered domain per week that Let's Encrypt allows.

Split horizon: the same name, two answers#

Split horizon means your internal resolver answers photos.example.com with 192.168.1.20 while the rest of the world gets 203.0.113.42. At home, traffic never leaves the LAN. Away, it comes in the front door, or over the mesh VPN if you took the Headscale route. The bookmark is the same, the certificate is valid in both cases, and nothing depends on your router supporting NAT hairpinning, which many cheap ones do not.

In dnsmasq, which is what Pi-hole runs underneath:

conf
# Answer for the name and everything under it, from the LAN
address=/home.example.com/192.168.1.20

# Override a single public name with a LAN address
host-record=photos.example.com,192.168.1.20

# Send queries for a delegated internal zone to a different server
server=/lab.example.com/192.168.1.53

# Do not discard private answers for names in this domain
rebind-domain-ok=/example.com/

In AdGuard Home, the equivalent lives in AdGuardHome.yaml as rewrites, which do support wildcard patterns:

yaml
filtering:
  rewrites:
    - domain: '*.home.example.com'
      answer: 192.168.1.20
    - domain: photos.example.com
      answer: 192.168.1.20
dns:
  upstream_dns:
    - https://dns.quad9.net/dns-query
    - '[/lab.example.com/]192.168.1.53'

Remember that AdGuard Home rewrites its own YAML while running, so edit it with the service stopped or you will lose the change. And in Pi-hole v6 the configuration moved wholesale into /etc/pihole/pihole.toml, so guides that tell you to edit setupVars.conf are describing software that no longer exists.

DNS rebinding protection will break this, once#

The first time you set up split horizon with public records pointing at private addresses, something in the chain will refuse to pass the answer through. Resolvers drop upstream responses containing RFC 1918 addresses because that is the signature of a DNS rebinding attack, where a hostile site resolves its own name to your router's address to reach it from inside your browser's origin.

The protection is correct and it is on by default in a lot of places, including OpenWrt, most consumer router firmware, and Unbound. The symptom is that dig against your Pi-hole returns the right answer while a laptop using the router's DNS gets nothing, or that resolution works from one VLAN and not another.

Allowlist your own domain rather than turning the protection off globally: rebind-domain-ok=/example.com/ for dnsmasq, private-domain: "example.com" for Unbound, option rebind_domain 'example.com' on OpenWrt. If a router refuses to expose the setting, stop using it as a resolver and point DHCP at your own.

TTLs, for when you are moving things#

TTL is a promise about how long an answer can be cached, and you cannot shorten it retroactively. The sequence for any migration is:

bash
# 1. At least one current-TTL period before the move, drop the TTL
#    (do this a day ahead if the record is on the default 3600)
# 2. Verify what resolvers are actually handing out
dig +noall +answer photos.example.com @1.1.1.1
dig +noall +answer photos.example.com @192.168.1.53

# 3. Watch the countdown to confirm caching behaviour
watch -n5 "dig +noall +answer photos.example.com @1.1.1.1"

# 4. Make the change, confirm from several resolvers, then raise the TTL back
dig +trace photos.example.com | tail -20

Two things TTL does not cover. Negative answers are cached according to the zone's SOA minimum field, not the record TTL, so an NXDOMAIN you have just fixed can outlive the fix. And browsers keep their own short DNS cache regardless of what the resolver says, so "it works in curl but not in Chrome" for the first minute is normal, not a sign that something is wrong.

Lower TTLs to 300 during any migration and put them back to 3600 afterwards. Permanently low TTLs cost you a query per client per five minutes and make your resolver a single point of failure with a very short fuse.

Blocker or real DNS server#

Two different jobs, and most people only need the first.

Blocker (Pi-hole, AdGuard Home)Authoritative server (Technitium DNS Server)
Forwards and cachesYesYes
Blocklists, per-client policyYes, this is the pointYes
Local overrides and wildcardsYesYes
Hosts a real zone with zone filesNoYes
DNSSEC signing, AXFR to a secondaryNoYes
Idle memory100 MB / 60 MB130 MB
Ops load2 of 53 of 5

Choose Pi-hole for group-based per-device rules and the larger blocklist ecosystem, AdGuard Home when you want DNS-over-HTTPS and DNS-over-TLS both as an upstream client and as a server without adding a second daemon. Pi-hole vs AdGuard Home has the detail.

Move to Technitium DNS Server when you actually own a domain and want to stop clicking through someone else's DNS console: it will sign a zone with DNSSEC from the UI, transfer to a secondary with NOTIFY, and do conditional forwarding and per-zone policy properly. Two defaults to change on day one: it ships with admin / admin on the web console at 5380, and recursion is restricted by network ACL, so a second VLAN or a VPN range gets REFUSED until you widen it.

Whichever you run, run two. A single resolver is the one outage that makes the entire house believe the internet is down, and it is the outage you will cause yourself during an upgrade. Two instances with both addresses in DHCP costs 60 MB and a second container.

What to do next#

Buy the domain, create the wildcard records, then go and set up the proxy that uses them: Reverse proxy and TLS covers the DNS-01 challenge and the wildcard certificate that makes internal names work without warnings. If you want those names to work from outside the house as well, Remote access without port forwarding explains which of the four remote access routes keeps the hostnames consistent. And if you are still deciding the shape of the whole thing, Your first self-hosted server covers the other decisions that are hard to undo.

Questions#

Why can't I use .local for my home server?

RFC 6762 reserves .local. for multicast DNS: any name ending in .local. is defined as link-local and is supposed to be resolved by multicast to 224.0.0.251 rather than by asking a DNS server. macOS, iOS and Linux machines running Avahi honour that and never send the query to your resolver, while Windows and Android behave differently again. The result is a name that works on some devices and not others, with no error message that explains why.

What about .lan, .home or .internal?

They resolve fine on your own network and they are permanently second class. No certificate authority will ever issue for a name you cannot prove you own, so every browser shows a warning forever unless you run a private CA and install its root on every device including phones and TVs. home.arpa (RFC 8375) and .internal, which ICANN identified as the string for private-use applications, are the two defensible choices if you truly refuse to buy a domain. An invented string is not: .dev was a popular fake local suffix until it became a real gTLD with HSTS preloaded across the whole TLD.

How do wildcard DNS records work?

A record like *.home.example.com A 192.168.1.20 answers for any single label under that name, so every new service resolves without touching DNS again. Two rules catch people. A wildcard covers exactly one label, so *.example.com does not answer for a.b.example.com. And an explicit record always wins, so if nas.home.example.com exists as its own A record, the wildcard is never consulted for it.

Why does my internal hostname resolve everywhere except on my router's DNS?

DNS rebinding protection. Resolvers including dnsmasq, Unbound and most consumer router firmware discard answers from public DNS that point at private address space, because that is the shape of a rebinding attack. Your public A record pointing at 192.168.1.20 is exactly that shape. Fix it by allowlisting the domain: rebind-domain-ok=/example.com/ in dnsmasq, private-domain: "example.com" in Unbound, or the equivalent option in the router UI.

Do I need a real DNS server or is Pi-hole enough?

Pi-hole and AdGuard Home are forwarding resolvers with blocklists and a local override table. That is enough for split horizon, local names and wildcards, and it is what most people should run. You need Technitium DNS Server when you want to be authoritative for a zone: real zone files, DNSSEC signing from the UI, zone transfers to a secondary, and per-zone policy that an override table cannot express.

How long before a DNS change takes effect?

Up to the record's TTL, plus however long the client caches. Lower the TTL to 300 seconds at least one full old-TTL period before you move anything, make the change, confirm it, then raise it again. Also budget for negative caching, which is governed by the zone's SOA minimum and not by the record TTL, so an NXDOMAIN you just fixed can persist longer than the record would have.

Is it worth paying for a domain just for a home lab?

It is the highest-value spend in self-hosting. For roughly the price of one month of a streaming service per year you get publicly trusted certificates on internal-only names through DNS-01, stable OIDC redirect URIs, service names that survive an ISP change, and an email domain that is not tied to a provider. Every workaround for not having one costs more time than the domain costs money.

Sources#

Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.