Skip to content

Context-Aware DNS

Some apps accept exactly one domain, and Nextcloud is the one that pushed me to do this. Context-aware DNS lets that single domain resolve to different IPs depending on where the client is: the local LAN IP on the home network, the Tailscale IP on the tailnet, and the public Cloudflare IP elsewhere. To do that you need two DNS servers on the same box, which means macvlan so each can own port 53 with its own LAN IP.

Why two DNS servers

  • AdGuard Home serves the local network — resolves nextcloud.example.com → the server's LAN IP.
  • Pi-hole serves the tailnet — resolves the same name → the server's Tailscale IP.

Both need port 53, which the host already occupies, so both run on a macvlan network with their own IPs. A Tailscale sidecar advertises the Pi-hole macvlan IP to the tailnet.

services:
  adguardhome:
    image: adguard/adguardhome
    networks:
      macvlan_net:
        ipv4_address: 192.168.4.100

  pihole:
    image: pihole/pihole:latest
    environment:
      FTLCONF_dns_listeningMode: 'all'
    networks:
      macvlan_net:
        ipv4_address: 192.168.4.101

  tailscale:
    image: tailscale/tailscale:latest
    environment:
      TS_AUTHKEY: ${tailscale_auth_key}
      TS_ACCEPT_DNS: "true"
      TS_ROUTES: 192.168.4.101/32     # advertise Pi-hole to the tailnet
    networks:
      macvlan_net:
        ipv4_address: 192.168.4.103
    cap_add: [NET_ADMIN, SYS_MODULE]

networks:
  macvlan_net:
    driver: macvlan
    driver_opts:
      parent: eno1
    ipam:
      config:
        - subnet: 192.168.4.0/24
          gateway: 192.168.4.1

Macvlan basics, the host↔container link, and the systemd persistence service are covered in Docker Networking and Ad Blocking.

DNS records

AdGuard Home (Filters → DNS Rewrites):

Domain Answer
nextcloud.example.com 192.168.4.142 (LAN IP)
collabora.example.com LAN IP
onlyoffice.example.com LAN IP

Pi-hole (same records, tailnet answers):

Domain Answer
nextcloud.example.com 100.x.x.x (Tailscale IP)
collabora.example.com Tailscale IP
onlyoffice.example.com Tailscale IP

In the Tailscale admin console, set the tailnet nameserver to Pi-hole (192.168.4.101) with Override DNS servers, and make sure Windows/mobile clients use Tailscale DNS in the app.

Client + container notes

  • Windows: DNS server assignment must be Automatic for the domain to resolve locally.
  • Android: Private DNS must be Automatic for local resolution.
  • App containers (Nextcloud, Collabora) must point at the local DNS server so they can resolve the domain themselves:
    dns:
      - 192.168.4.100

Collabora runs with SSL termination handled by Nginx (--o:ssl.enable=false --o:ssl.termination=true), and certs for collabora.example.com / onlyoffice.example.com come from the certbot container via the Cloudflare DNS plugin.

Why this matters

One canonical domain, three contexts — local clients hit the LAN directly, tailnet clients route through Tailscale, and public clients go through Cloudflare. No app reconfiguration, no second domain, no split-brain. Nextcloud is the consumer example; the pattern generalizes to any single-domain app.

See also