Split DNS for Self-Hosted Services
What Is Split DNS?
Split DNS (also called split-horizon DNS) means answering the same domain name query differently depending on where the query comes from. The most common self-hosting use case: when you’re at home, nextcloud.yourdomain.com resolves to your server’s local IP (192.168.1.10). When you’re away, it resolves to your public IP or Cloudflare Tunnel endpoint.
Without split DNS, accessing your services from inside your network means your traffic goes out to the internet and back in through your firewall (hairpin NAT) — slower, sometimes broken, and wasteful. Split DNS solves this by short-circuiting local requests to stay on the LAN.
Prerequisites
- A DNS server running on your network (Pi-hole, AdGuard Home, CoreDNS, or Unbound)
- Your router configured to use your DNS server as the primary resolver
- A domain name pointing to your server externally (via DNS provider or Cloudflare Tunnel)
Why You Need Split DNS
| Scenario | Without Split DNS | With Split DNS |
|---|---|---|
Access nextcloud.yourdomain.com from home | Traffic → internet → firewall → server (hairpin NAT) | Traffic → server directly (LAN) |
| Router doesn’t support hairpin NAT | Connection fails | Works — no hairpin needed |
| Access speed from LAN | ~50-100ms latency (internet round-trip) | <1ms latency (LAN) |
| External DNS goes down | Can’t reach local services | Local DNS still resolves internal domains |
| VPN users accessing internal services | Need separate hostnames | Same hostname works everywhere |
Method 1: DNS Rewrites (Simplest)
The easiest approach — manually override specific domains in your DNS server to resolve to local IPs.
Pi-hole
Create a custom dnsmasq config:
cat > /etc/dnsmasq.d/05-split-dns.conf << 'EOF'
# Override public domains to local IPs
address=/nextcloud.yourdomain.com/192.168.1.10
address=/jellyfin.yourdomain.com/192.168.1.10
address=/grafana.yourdomain.com/192.168.1.10
# Wildcard: all subdomains of yourdomain.com → local server
address=/.yourdomain.com/192.168.1.10
EOF
Restart Pi-hole’s DNS:
docker exec pihole pihole restartdns
See Pi-hole as a DNS Server for detailed configuration.
AdGuard Home
Add DNS rewrites via the web UI (Filters > DNS rewrites):
| Domain | Answer |
|---|---|
nextcloud.yourdomain.com | 192.168.1.10 |
jellyfin.yourdomain.com | 192.168.1.10 |
*.yourdomain.com | 192.168.1.10 |
Or via AdGuardHome.yaml:
filtering:
rewrites:
- domain: "*.yourdomain.com"
answer: "192.168.1.10"
AdGuard Home supports wildcard rewrites natively — no extra config files needed. See AdGuard Home as a DNS Server for details.
Unbound
Add local zone entries to your a-records.conf:
local-data: "nextcloud.yourdomain.com. A 192.168.1.10"
local-data: "jellyfin.yourdomain.com. A 192.168.1.10"
local-data: "grafana.yourdomain.com. A 192.168.1.10"
For wildcard coverage, add a local zone:
local-zone: "yourdomain.com." redirect
local-data: "yourdomain.com. A 192.168.1.10"
See Unbound setup guide for configuration details.
Method 2: Conditional Forwarding (Multi-Network)
When you have multiple DNS zones — internal services, a corporate VPN, and public DNS — use conditional forwarding to route each to the right resolver.
CoreDNS
CoreDNS’s Corefile makes split DNS explicit:
# Internal services → local resolution
yourdomain.com {
hosts /etc/coredns/internal-hosts {
fallthrough
}
forward . 192.168.1.1 # Fallback to router
log
}
# Corporate VPN domains → corporate DNS
corp.example.com {
forward . 10.0.0.53
cache 600
}
# Home LAN names → router
home.lan {
forward . 192.168.1.1
cache 600
}
# Everything else → public DNS (encrypted)
. {
forward . tls://1.1.1.1 tls://9.9.9.9 {
tls_servername cloudflare-dns.com
}
cache 3600
health localhost:8080
log
}
Create /etc/coredns/internal-hosts:
192.168.1.10 nextcloud.yourdomain.com
192.168.1.10 jellyfin.yourdomain.com
192.168.1.10 grafana.yourdomain.com
192.168.1.10 portainer.yourdomain.com
See CoreDNS setup guide for full Corefile configuration.
AdGuard Home Upstream Routing
[/corp.example.com/]10.0.0.53
[/home.lan/]192.168.1.1
https://cloudflare-dns.com/dns-query
Queries for corp.example.com → corporate DNS. Queries for home.lan → router. Everything else → Cloudflare DoH.
Method 3: Wildcard DNS + Reverse Proxy (Most Flexible)
Combine wildcard DNS with a reverse proxy for the cleanest setup:
- DNS: All
*.yourdomain.comresolves to your server locally - Reverse proxy: Routes each subdomain to the correct container
- External: Same domain resolves to your public IP or Cloudflare Tunnel
Architecture
Local client:
*.yourdomain.com → [DNS: 192.168.1.10] → Reverse Proxy → Container
External client:
*.yourdomain.com → [Public DNS: public IP] → Firewall/Tunnel → Reverse Proxy → Container
Implementation
Local DNS (any method): Set *.yourdomain.com → 192.168.1.10
Reverse proxy (Nginx Proxy Manager, Caddy, or Traefik):
# Caddy example
nextcloud.yourdomain.com {
reverse_proxy nextcloud:80
}
jellyfin.yourdomain.com {
reverse_proxy jellyfin:8096
}
External DNS (at your domain registrar/Cloudflare): Set *.yourdomain.com → your public IP or CNAME to your tunnel.
This works because local devices resolve to the local IP (hitting the reverse proxy directly), while external devices resolve to the public IP (hitting the reverse proxy through the firewall or tunnel). The reverse proxy handles both cases identically.
Common Patterns
Pattern 1: Home Lab With One Server
Everything runs on a single Docker host at 192.168.1.10.
DNS: *.yourdomain.com → 192.168.1.10
Reverse proxy: Routes subdomains to containers
External access: Cloudflare Tunnel or WireGuard VPN
Pattern 2: Multiple Servers
Services spread across multiple machines.
DNS entries:
nextcloud.yourdomain.com → 192.168.1.10 (Docker host 1)
proxmox.yourdomain.com → 192.168.1.50 (Hypervisor)
nas.yourdomain.com → 192.168.1.100 (NAS)
Pattern 3: VPN + Local
Internal services resolve locally. VPN users also get local resolution through the VPN’s DNS.
Tailscale/WireGuard DNS: Point VPN clients at your internal DNS server. All queries resolve identically whether on LAN or VPN.
See Remote Access for VPN-based approaches.
Testing Split DNS
Verify your split DNS is working:
# Test from inside your network
dig @192.168.1.10 nextcloud.yourdomain.com
# Expected: 192.168.1.10 (local IP)
# Test what public DNS returns
dig @1.1.1.1 nextcloud.yourdomain.com
# Expected: your public IP or CNAME
# Verify no hairpin NAT
traceroute nextcloud.yourdomain.com
# Expected: 1 hop (direct to LAN IP), not multiple hops through ISP
Common Mistakes
Forgetting to Disable Secondary DNS
If your router distributes both Pi-hole AND a public DNS as DNS servers, clients may bypass your split DNS entirely. Remove secondary DNS servers.
Not Covering All Subdomains
Adding nextcloud.yourdomain.com but forgetting collabora.yourdomain.com (which Nextcloud needs for document editing) breaks functionality. Use wildcard DNS (*.yourdomain.com) to avoid this.
SSL Certificate Issues
Your SSL certificates must be valid for the domain regardless of how it resolves. Certificates from Let’s Encrypt using DNS challenge validation work perfectly with split DNS. HTTP challenge validation may fail if the cert provider can’t reach your local server.
Docker DNS Override
Containers use Docker’s internal DNS by default, not your host’s DNS. If a container needs to resolve your split DNS names, configure Docker’s DNS:
services:
myapp:
dns:
- 192.168.1.10 # Your split DNS server
Or set it globally in /etc/docker/daemon.json:
{
"dns": ["192.168.1.10"]
}
Next Steps
- Set up a reverse proxy: Nginx Proxy Manager or Caddy
- Enable encrypted DNS: Encrypted DNS Guide
- Configure remote access: Remote Access
- Learn DNS fundamentals: DNS Explained
Related
Get self-hosting tips in your inbox
Get the Docker Compose configs, hardware picks, and setup shortcuts we don't put in articles. Weekly. No spam.
Comments