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

Why You Need Split DNS

ScenarioWithout Split DNSWith Split DNS
Access nextcloud.yourdomain.com from homeTraffic → internet → firewall → server (hairpin NAT)Traffic → server directly (LAN)
Router doesn’t support hairpin NATConnection failsWorks — no hairpin needed
Access speed from LAN~50-100ms latency (internet round-trip)<1ms latency (LAN)
External DNS goes downCan’t reach local servicesLocal DNS still resolves internal domains
VPN users accessing internal servicesNeed separate hostnamesSame 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):

DomainAnswer
nextcloud.yourdomain.com192.168.1.10
jellyfin.yourdomain.com192.168.1.10
*.yourdomain.com192.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:

  1. DNS: All *.yourdomain.com resolves to your server locally
  2. Reverse proxy: Routes each subdomain to the correct container
  3. 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.com192.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.com192.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

Comments