Your Own DNS Resolver: Why You Need One, Which to Choose, and How to Set Up Unbound in 10 Minutes.
Your Own DNS Resolver: Why You Need One, Which to Choose, and How to Set Up Unbound in 10 Minutes.
Every time your server updates packages, checks a certificate, connects to an external API, or sends metrics to a monitoring system — it resolves a domain. And that request goes to 1.1.1.1 or 8.8.8.8. Cloudflare sees that your server hit the package repository at 3 AM, then a payment API, then a threat database. A complete map of your infrastructure — no hacking required, just DNS logs.
Your own resolver fixes this. Requests are handled locally, cached on the server, and only what’s not in cache goes outside — and even then, encrypted.
WHY YOUR OWN RESOLVER
Privacy — the most obvious reason. An external DNS provider sees every domain your server contacts. This isn’t paranoia — it’s real data that gets collected, analyzed, and can leak. Your own resolver keeps that information with you.
Independence — less obvious but more critical. 1.1.1.1 is reliable, but it’s someone else’s server. In October 2021, Facebook’s own DNS servers went down — and everything that depended on them stopped working. 1.1.1.1 was fine that day, but tomorrow it might be your provider’s turn. Your own resolver goes directly to root servers and depends on nobody.
How this works: root servers are the top of the DNS hierarchy. There are 13 groups of them, they’re essentially always available, and they know where to find any domain. A regular resolver like 1.1.1.1 walks up to them and caches the responses — you’re using its cache. Your own resolver does the same thing, but just for you — with no dependency on anyone else’s cache.
Speed — a pleasant bonus. Frequently queried domains settle into the local cache. npm registry, GitHub, Docker Hub — all of these resolve in single-digit milliseconds without going out to the internet.
Control — what opens up new possibilities. Want to block a specific domain at the DNS level? Add internal records for your local network? Log all of the server’s DNS queries for auditing? With your own resolver, all of this is done in config.
Blocking domains via local-zone is one of the simplest and most useful features. Add to the config:
server:
# Block a domain — return REFUSED
local-zone: "example.com" refuse
# Block ad networks
local-zone: "doubleclick.net" refuse
local-zone: "googlesyndication.com" refuse
# Redirect to your own IP (for internal services)
local-zone: "internal.example.com" static
local-data: "internal.example.com A 10.0.0.5"
refuse — the resolver returns REFUSED, the browser shows a DNS error. static with local-data — returns the specified IP instead of the real one. Useful for internal services that should resolve to a private IP.
THREE OPTIONS: UNBOUND, BIND, POWERDNS
The choice of resolver depends on the task. All three solve different problems.
Unbound — a specialized caching resolver. It doesn’t try to do everything at once: it accepts queries, finds answers from root servers, caches results, validates DNSSEC. Takes 10 minutes to configure, memory usage depends on cache size — around 150-200 MB with the configuration in this article, runs stable for years without intervention. If you need a local resolver for a server or small network — this is the right choice.
Bind (named) — the industry standard that’s been around since the 1980s. Powers most of the world’s DNS infrastructure. Can simultaneously be a caching resolver and an authoritative server for your domains. Supports GeoDNS — different responses for users from different countries. Supports split-horizon — different responses for internal and external networks. A powerful tool with the corresponding learning curve.
PowerDNS — the modern option for those who want to manage DNS programmatically. Zones are stored in MySQL or PostgreSQL, records are managed via REST API. You can write a script that automatically creates DNS records when a new server launches. The choice for automated systems and those who don’t want to edit text files by hand.
For a local resolver on a single server — Unbound. For a full DNS server with zones — Bind. For dynamic management via API — PowerDNS.
UNBOUND: INSTALLATION AND CONFIGURATION
Installation is a single command:
apt install unbound # Debian/Ubuntu
dnf install unbound # RHEL/Fedora
After installation, Unbound listens on 127.0.0.1:53 and is ready to accept queries. But the base configuration needs a few tweaks.
Open the configuration file:
nano /etc/unbound/unbound.conf
Minimal working configuration:
server:
# Listen only on localhost
interface: 127.0.0.1
port: 53
# Allow queries only from localhost
access-control: 127.0.0.1/32 allow
access-control: 0.0.0.0/0 refuse
# DNSSEC
auto-trust-anchor-file: "/var/lib/unbound/root.key"
# Hide version and identity
hide-version: yes
hide-identity: yes
# Cache
cache-min-ttl: 300
cache-max-ttl: 86400
msg-cache-size: 50m
rrset-cache-size: 100m
# Performance
num-threads: 2
so-reuseport: yes
# Logging
verbosity: 1
log-queries: no
Download the DNSSEC root anchors:
unbound-anchor -a /var/lib/unbound/root.key
Check the configuration and start:
unbound-checkconf
systemctl enable --now unbound
Verify it’s working:
dig @127.0.0.1 cloudflare.com
dig @127.0.0.1 cloudflare.com +dnssec
The second query with +dnssec should return records with the ad flag (authenticated data) — which means DNSSEC validation passed.
CONNECTING TO THE SYSTEM RESOLVER
Unbound is running, but the server isn’t using it yet — systemd-resolved is still sending requests to 1.1.1.1. You need to switch the system DNS to 127.0.0.1.
Important: DNSSEC and DoT in systemd-resolved need to be disabled. If you leave them enabled — systemd-resolved will try to validate DNSSEC and encrypt queries on top of what Unbound is already doing. Double work, and worse — a configuration conflict. Unbound handles all of this itself; systemd-resolved is just a pass-through here.
# /etc/systemd/resolved.conf
[Resolve]
DNS=127.0.0.1
DNSOverTLS=no
DNSSEC=no
systemctl restart systemd-resolved
resolvectl status | grep "Current DNS Server"
Should show 127.0.0.1. If you see 1.1.1.1 or a Hetzner IP — something went wrong, check netplan or /etc/resolv.conf.
Verify that resolution is going through Unbound:
resolvectl query cloudflare.com
DOT UPSTREAM: ENCRYPTING OUTGOING QUERIES
The local resolver solves the problem inside the server — queries don’t go to someone else’s resolver. But when Unbound doesn’t find an answer in cache, it still goes to external servers — forwarders. That outgoing traffic goes in plain text over UDP on port 53 by default. Meaning your provider can still see which domains your server is resolving — just not constantly, only on cache misses.
DoT upstream encrypts exactly these outgoing queries. The provider sees encrypted traffic on port 853 — but not which domains you’re resolving.
Add to the configuration:
forward-zone:
name: "."
forward-tls-upstream: yes
forward-addr: 1.1.1.1@853#cloudflare-dns.com
forward-addr: 8.8.8.8@853#dns.google
forward-addr: 9.9.9.9@853#dns.quad9.net
Restart:
systemctl restart unbound
Now Unbound caches locally, and queries that aren’t in cache get encrypted via DoT and forwarded to the configured forwarders — 1.1.1.1, 8.8.8.8, or 9.9.9.9. The provider sees only encrypted traffic on port 853.
VERIFICATION AND DEBUGGING
Cache statistics:
unbound-control stats_noreset | grep -E "total|cache"
How many queries were processed and what percentage came from cache — this shows how efficiently the resolver is working.
DNSSEC test:
# This domain should return SERVFAIL — it's intentionally broken for testing
dig @127.0.0.1 dnssec-failed.org
# This should resolve normally
dig @127.0.0.1 cloudflare.com +dnssec
If dnssec-failed.org returns SERVFAIL — DNSSEC validation is working correctly.
Logs:
journalctl -u unbound -f
USING YOUR RESOLVER FROM A HOME COMPUTER
Unbound in the base configuration only listens on 127.0.0.1 — accessible exclusively from the server itself. But it can be safely opened to a specific IP — for example, your home computer. Then instead of 8.8.8.8 in your network settings, you put your server’s IP, and all DNS traffic goes through your resolver.
Find your home IP:
curl ifconfig.me
Open Unbound for that IP — add to the config:
server:
interface: 0.0.0.0
access-control: 0.0.0.0/0 refuse
access-control: 127.0.0.1/32 allow
access-control: YOUR_HOME_IP/32 allow
Open port 53 in nftables only for your IP — no other way:
ip saddr YOUR_HOME_IP udp dport 53 accept
ip saddr YOUR_HOME_IP tcp dport 53 accept
Restart Unbound:
systemctl restart unbound
Verify from your home computer that the resolver responds:
dig @YOUR_SERVER_IP cloudflare.com
If it responds — set your server’s IP as the DNS in your network adapter settings.
Important: your home IP is usually dynamic — it changes when you reconnect. If the IP changes, the resolver will stop responding. You’ll need to update the nftables rule and Unbound config. For permanent use, get a static IP from your provider or use Dynamic DNS.
WHEN YOU NEED BIND OR POWERDNS
Unbound is a resolver. It can’t be an authoritative server for your domains, doesn’t support zones, doesn’t do GeoDNS.
If you need to host your own DNS zones, configure split-horizon DNS, implement GeoDNS with different responses for different countries, or build a full DNS infrastructure — that’s a job for Bind. Bind can simultaneously be a resolver and an authoritative server, supports complex ACLs and view configurations for separating internal and external zones.
PowerDNS is the choice when DNS needs to be administered programmatically: zones are stored in a database, records are managed via REST API, you can write a custom backend. It’s the choice for automated systems and cloud providers.
Detailed Bind configuration with practical examples — zones, GeoDNS, split-horizon — is covered in the DNS server configuration course: two lessons with full depth from basic configuration to advanced features.
CONCLUSION
Your own resolver isn’t rocket science. Unbound installs in five minutes, requires 150-200 MB RAM with a standard cache configuration, and immediately gives you privacy, independence from external providers, and local caching.
For most servers, that’s enough. If your needs grow to the level of hosting your own zones and complex DNS traffic routing — the next step is Bind.
