A lightweight DNS resolver that automatically discovers services from a Pangolin reverse proxy and resolves their domains to a local IP address — avoiding hairpin NAT when clients and services are on the same LAN.
When you run Pangolin as a reverse proxy in your homelab, all traffic to your services (e.g. app.example.com) goes through the public internet and back via hairpin NAT, even if both the client and Pangolin are on the same local network. This adds unnecessary latency and external bandwidth usage.
pangolin-dns is a single Go binary that acts as a DNS server. It polls the Pangolin Integration API to discover all configured resources and their domains, then resolves those domains to the local Pangolin IP. All other queries are forwarded to an upstream DNS server (e.g. 1.1.1.1).
Client DNS query: app.example.com
→ pangolin-dns checks local store
→ Found! Returns 10.1.100.2 (local Pangolin IP)
Client DNS query: google.com
→ pangolin-dns checks local store
→ Not found, forwards to upstream (1.1.1.1)
- Auto-discovery — polls the Pangolin Integration API and picks up new resources automatically
- Local prefix — optionally creates
local.{domain}entries as an explicit local alternative - Upstream forwarding — non-Pangolin domains are forwarded to a configurable upstream DNS
- Lightweight — single static Go binary, ~10MB Docker image
- Zero config for domains — no manual domain list needed, everything comes from Pangolin
- Health endpoint —
GET /healthzon port 8080 reports record count, last poll time and error count
┌──────────────────────────────────────────┐
│ pangolin-dns │
│ │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ DNS Server │ │ Pangolin Poller │ │
│ │ (miekg/dns) │ │ (HTTP client) │ │
│ │ :53 UDP/TCP │ │ every 60s │ │
│ └──────┬──────┘ └────────┬─────────┘ │
│ │ ┌────────────┐ │ │
│ └──│Record Store│───┘ │
│ │ (in-memory)│ │
│ └────────────┘ │
└──────────────────────────────────────────┘
- Pangolin with the Integration API enabled (port 3004)
- An API key (root key for org auto-discovery, or org-scoped key with
PANGOLIN_ORG_IDset)
-
Create a
.envfile:PANGOLIN_API_KEY=your_api_key_id.your_api_key_secret -
Start the container (pulls the pre-built image from Docker Hub automatically):
docker compose up -d
-
Test it:
nslookup app.example.com localhost
go build -o pangolin-dns .
PANGOLIN_API_KEY=your_key ./pangolin-dnsAll configuration is done via environment variables:
| Variable | Default | Description |
|---|---|---|
PANGOLIN_API_URL |
http://10.1.100.2:3004 |
Pangolin Integration API URL |
PANGOLIN_API_KEY |
(required) | API key (keyId.keySecret) |
PANGOLIN_LOCAL_IP |
10.1.100.2 |
IP to resolve Pangolin domains to |
PANGOLIN_ORG_ID |
(auto-discover) | Specific org ID (skip auto-discovery) |
UPSTREAM_DNS |
1.1.1.1:53 |
Upstream DNS server for non-local queries |
POLL_INTERVAL |
60s |
How often to poll the Pangolin API |
DNS_PORT |
53 |
DNS server listen port |
HEALTH_PORT |
8080 |
HTTP health endpoint port |
ENABLE_LOCAL_PREFIX |
true |
Create local.{domain} entries |
pangolin-dns runs as a Docker container on any host that is reachable from your LAN — typically the same machine as Pangolin itself.
PANGOLIN_API_KEY=your_api_key_id.your_api_key_secret
The API key is created in the Pangolin admin UI under Settings → API Keys. Use a root key to allow auto-discovery of all organizations, or set PANGOLIN_ORG_ID and use an org-scoped key.
The defaults assume Pangolin runs at 10.1.100.2. If your setup differs, override the relevant variables:
environment:
- PANGOLIN_API_URL=http://<your-pangolin-ip>:3004
- PANGOLIN_LOCAL_IP=<your-pangolin-ip>docker compose up -d# Check the health endpoint
curl http://<host-ip>:8080/healthz
# Test DNS resolution (replace with one of your actual Pangolin domains)
nslookup app.example.com <host-ip>The health response looks like:
{"status":"ok","records":12,"last_poll":"2026-02-20T19:00:00Z","poll_errors":0}records should be > 0 after the first poll (within a few seconds of startup).
Other useful endpoints:
| Endpoint | Method | Description |
|---|---|---|
/healthz |
GET | Service health, record count, last poll time |
/domains |
GET | List all currently active DNS records |
/poll |
POST | Trigger an immediate re-poll of the Pangolin API |
# See which domains are registered
curl http://<host-ip>:8080/domains
# Force immediate update after adding a new Pangolin service
curl -X POST http://<host-ip>:8080/pollpangolin-dns only does something useful if your devices actually use it as their DNS resolver. You have two options:
Configure your router to hand out the pangolin-dns host IP as the DNS server via DHCP. New DHCP leases will pick it up immediately; existing clients will update on their next lease renewal (or after a reconnect).
UniFi (UDM / UDM Pro / USG):
- Go to Settings → Networks → [your LAN network] → Advanced
- Under DHCP Name Server, select Manual
- Enter the IP of the host running pangolin-dns as DNS Server 1
- Optionally add
1.1.1.1as DNS Server 2 as a fallback (pangolin-dns already forwards unknown queries upstream, so this is only needed if pangolin-dns itself goes down) - Save — clients will receive the new DNS on their next DHCP renewal
pfSense / OPNsense:
- Go to Services → DHCP Server → [your LAN interface]
- Set DNS Servers to the pangolin-dns host IP
- Save and apply
Generic router (most home routers):
Look for LAN / DHCP Settings and set the Primary DNS to the pangolin-dns host IP. The exact location varies by router brand.
Set the DNS server manually on the device you want to configure. pangolin-dns forwards all non-Pangolin queries upstream, so it is safe to use as your only DNS resolver.
Windows:
- Open Settings → Network & Internet → [your adapter] → Edit DNS
- Switch to Manual, enable IPv4
- Set Preferred DNS to the pangolin-dns host IP
- Set Alternate DNS to
1.1.1.1(fallback if pangolin-dns is unreachable)
Or via PowerShell (replace Ethernet and the IP as needed):
Set-DnsClientServerAddress -InterfaceAlias "Ethernet" -ServerAddresses "10.1.100.2","1.1.1.1"macOS:
- Open System Settings → Network → [your connection] → Details → DNS
- Click + and add the pangolin-dns host IP
- Click OK and Apply
Or via terminal:
# Replace "Wi-Fi" with your interface name from `networksetup -listallnetworkservices`
networksetup -setdnsservers "Wi-Fi" 10.1.100.2 1.1.1.1Linux (systemd-resolved):
Edit /etc/systemd/resolved.conf:
[Resolve]
DNS=10.1.100.2
FallbackDNS=1.1.1.1Then restart: sudo systemctl restart systemd-resolved
Or per-interface via NetworkManager:
nmcli connection modify "your-connection" ipv4.dns "10.1.100.2 1.1.1.1"
nmcli connection up "your-connection"# Should return the local Pangolin IP, not a public IP
nslookup app.example.com
# Check which DNS server answered
nslookup app.example.com 10.1.100.2If nslookup returns the PANGOLIN_LOCAL_IP you configured, everything is working.
MIT