The problem
Services carry public names (service.domain.fr) but live on a private
network. From the inside, resolving the public name raises two problems.
Traffic leaves only to come back: a pointless detour, and a dependency on
external connectivity. And for private services, the name simply must not
resolve publicly; someone still has to answer internally.
The naive solutions age badly: per-machine hosts files (guaranteed
drift, impossible on mobile), or exotic internal names (service.local)
that break TLS and force everything to be configured twice.
The pattern
An internal zone that is authoritative for the self-hosted domains, served by CoreDNS to private network clients only:
- Queries from internal clients go through the local resolver (split-horizon DNS).
- For the self-hosted zones, CoreDNS answers with the internal addresses, those of the private reverse proxy. Declarative, versioned configuration.
- Everything else is forwarded to public resolvers: the override stays surgical.
The same name therefore works everywhere (internal network, Internet, tailnet), and TLS remains valid since the wildcard certificate covers the public name in use.
The pitfalls
- Think about caches. A client that resolved the name before joining the network keeps its answer until the TTL expires. Transient inconsistencies are normal; knowing about them avoids chasing them for nothing.
- Do not leak the internal zone. The internal view is only served to internal clients, and the public DNS stays minimal.
- Document the invariant: every new self-hosted service gets its internal record. Otherwise the service works from the Internet but fails from inside the network, and debugging becomes counter-intuitive.
Why it’s interesting
It is a small setup, but it touches a real system invariant: name resolution is contextual. Understanding it means understanding a whole class of network bugs commonly dismissed as impossible.