Skip to main content

Overview

This guide walks you through setting up a private, secure DNS server using AdGuard Home (for ad-blocking and management) backed by Unbound (for recursive DNS resolution). This configuration enhances privacy by querying root DNS servers directly instead of relying on third-party providers.
Why this setup?
  • Privacy: No single DNS provider sees your entire query history
  • Security: Direct DNSSEC validation from root servers
  • Control: Custom blocklists and filtering rules
  • Performance: Intelligent caching and prefetching

Prerequisites

VM Setup

We recommend DigitalOcean for hosting. Create a new Droplet with the following spec:
  1. Droplet: Basic · Premium Intel
    • vCPU: 1
    • RAM: 2 GB
    • Disk: 70 GB SSD
    • Transfer: 2 TB
    • OS: Debian 13 x64
    • Cost: $16/mo ($0.024/hr)
  2. Create Droplet and reserve a Reserved IP address (Networking → Reserved IPs → assign to your Droplet)

DigitalOcean Firewall Rules

In the DigitalOcean control panel, go to Networking → Firewalls → Create Firewall and add the following Inbound Rules, then attach the firewall to your Droplet:
1

Core DNS & Web Traffic

Add these inbound rules using the preset types where available:
TypeProtocolPort RangeSources
SSHTCP22All IPv4, All IPv6
DNS TCPTCP53All IPv4, All IPv6
HTTPTCP80All IPv4, All IPv6
HTTPSTCP443All IPv4, All IPv6
CustomTCP853All IPv4, All IPv6
DNS UDPUDP53All IPv4, All IPv6
CustomUDP853All IPv4, All IPv6
2

Temporary Setup Access (delete-later)

Add one more Custom TCP inbound rule for the AdGuard Home setup wizard — remove this rule after setup is complete:
TypeProtocolPort RangeSources
CustomTCP3000Your home IP only
Label or tag this rule as delete-later so you remember to remove it once the setup wizard is finished.

Update Your Droplet

SSH into your new Droplet (use the IP shown in the DigitalOcean control panel or your Reserved IP) and prepare the system:
# Update system packages
sudo apt update
sudo apt upgrade -y
Press Y when prompted to confirm package upgrades. This may take a few minutes.

Step 1: Install AdGuard Home

AdGuard Home provides the web interface, ad-blocking, and DNS query logging.
wget --no-verbose -O - https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -v

Initial Configuration

  1. Navigate to http://[YOUR_EXTERNAL_IP]:3000 in your browser
  2. Proceed through the setup wizard to Step 3
Port 53 conflict? If you see bind: address already in use, run these commands to disable systemd-resolved:
sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolved
sudo rm -f /etc/resolv.conf && echo "nameserver 1.1.1.1" | sudo tee /etc/resolv.conf
Then restart AdGuard Home: sudo service AdGuardHome restart

DNS Settings Configuration

Configure Settings → DNS Settings with these values:
SettingValueWhy?
Upstream DNS servershttps://dns.cloudflare.com/dns-queryTemporary until Unbound is installed
Mode☑ Parallel requestsBest stability even with one upstream
Fallback DNS servershttps://dns.cloudflare.com/dns-queryBackup if primary fails
Rate limit15Limits queries per second per client (DoS protection)
Subnet prefix length (IPv4)32Tracks individual devices, not subnets
DNSSEC☑ Enable DNSSECValidates DNS responses aren’t tampered
IPv6 addresses☑ Disable resolvingPrevents timeouts if IPv6 isn’t configured
Blocking & Cache Settings:
SettingValueWhy?
Blocked response TTL86400 (24 hours)How long clients cache blocked domains
Cache size20000000 bytes (≈19MB)Initial cache; we’ll optimize later with Unbound
Override minimum TTL300 (5 minutes)Prevents too-frequent re-queries
Override maximum TTL86400 (24 hours)Limits stale data
Optimistic Caching☑ EnableServes cached data while refreshing in background
We’ll reconfigure caching in Step 3 after installing Unbound for optimal performance.

Add Blocklists

Go to Filters → DNS Blocklists and add these lists:
  • Name: hapara.fail Blocklist
  • URL: https://cdn.jsdelivr.net/gh/hapara-fail/blocklist@main/blocklist.txt
Our custom blocklist targeting surveillance and tracking domains.
Click Save after adding each list. AdGuard will download and compile them.

Step 2: Set Up Unbound (Recursive Resolver)

Unbound replaces third-party DNS providers by querying root servers directly, enhancing privacy and control.

Install Unbound

sudo apt update
sudo apt install unbound -y

Configure Unbound

We’ll run Unbound on port 5335 to avoid conflicts with AdGuard Home (which uses port 53).
  1. Create the configuration file:
    sudo nano /etc/unbound/unbound.conf.d/recursive.conf
    
  2. Paste this optimized configuration:
server:
  # Logging
  verbosity: 0 # Minimal logging (use syslog)

  # Network Configuration
  port: 5335 # Non-standard port (AdGuard forwards here)
  interface: 127.0.0.1 # Listen on localhost only (security)
  do-ip4: yes
  do-udp: yes
  do-tcp: yes
  do-ip6: no # Disable IPv6 if not in use

  # Root Hints (list of root DNS servers)
  root-hints: "/var/lib/unbound/root.hints"

  # Security & Privacy
  harden-glue: yes # Only trust glue in-zone
  harden-dnssec-stripped: yes # Reject responses without DNSSEC
  use-caps-for-id: yes # Randomize query capitalization (security)
  edns-buffer-size: 1232 # Prevent fragmentation
  hide-identity: yes # Don't reveal server identity
  hide-version: yes # Don't reveal software version

  # Performance
  prefetch: yes # Refresh popular cached items before expiry
  num-threads: 1 # 1 for low-power VMs, 2+ for powerful servers
  so-rcvbuf: 1m # Socket receive buffer (performance)

  # Access Control
  access-control: 127.0.0.0/8 allow # Only localhost can query
  1. Save and exit (Ctrl+X, Y, Enter)
What is “prefetching”? Unbound refreshes frequently-used DNS records before they expire, keeping your most-visited sites blazing fast.

Download Root Hints

Root hints tell Unbound where the authoritative root DNS servers are:
sudo wget https://www.internic.net/domain/named.root -O /var/lib/unbound/root.hints
Optional but recommended: Auto-update root hints monthly via cron:
sudo crontab -e
Add this line to the bottom:
0 0 1 * * wget -O /var/lib/unbound/root.hints https://www.internic.net/domain/named.root
This cron job runs at midnight on the 1st of each month.

Start and Test Unbound

  1. Restart the service:
    sudo service unbound restart
    
  2. Check service status:
    sudo service unbound status
    
    Success: You should see active (running)
  3. Test DNS resolution:
    dig @127.0.0.1 -p 5335 google.com
    
    Success: Look for status: NOERROR and an IP address in the ANSWER SECTION
Troubleshooting: If you see connection timed out or SERVFAIL, check your configuration file for syntax errors:
sudo unbound-checkconf /etc/unbound/unbound.conf.d/recursive.conf

Step 3: Connect AdGuard Home to Unbound

Now we’ll point AdGuard Home to use your local Unbound instance instead of Cloudflare.

Update Upstream DNS

  1. In AdGuard Home, go to Settings → DNS Settings
  2. Upstream DNS servers:
    • Delete all existing entries
    • Add only: 127.0.0.1:5335
  3. Parallel requests: Select Parallel requests (recommended for stability)
  4. Bootstrap DNS servers: These resolve IPs for DNS-over-HTTPS/TLS hostnames (not needed for our IP-based upstream, but good practice):
    9.9.9.9
    1.1.1.1
    
  5. Private Reverse DNS servers: (Optional) Point to your router if you want local hostname resolution (e.g., 192.168.1.1)
  6. DNSSEC: ☑ Enable (Unbound validates, but this provides a second check)
  7. Click Test Upstreams → Should show “Server is working” ✅
  8. Click Apply

Optimize Cache Settings

Since Unbound has superior caching with prefetching, we’ll minimize AdGuard’s cache:
  1. Go to Settings → DNS Settings → DNS Cache Configuration
  2. Configure:
SettingValueWhy?
Cache size4194304 (4MB)Small cache keeps UI responsive
Override minimum TTL0 (empty/default)Let Unbound control TTL
Override maximum TTL0 (empty/default)Let Unbound control TTL
Optimistic cachingDisableUnbound’s prefetching is superior
Why disable optimistic caching? Unbound’s prefetch mechanism proactively refreshes popular records before they expire—more intelligent than AdGuard’s optimistic cache.

Step 4: Secure with SSL/TLS (DoH & DoT)

Enable encrypted DNS protocols: DNS-over-HTTPS (DoH) and DNS-over-TLS (DoT).
Prerequisite: You must have an A record pointing from your domain (e.g., dns.hapara.fail) to your VM’s external IP address before proceeding.

Install Certbot

sudo apt update
sudo apt install certbot -y

Obtain SSL Certificate

Let’s Encrypt needs port 80 to verify domain ownership. We’ll temporarily stop AdGuard Home:
# Stop AdGuard Home
sudo service AdGuardHome stop

# Request certificate (replace example.org with your domain)
sudo certbot certonly --standalone -d example.org

# Restart AdGuard Home
sudo service AdGuardHome start
Follow the Certbot prompts to enter your email and agree to the Terms of Service.

Configure Encryption in AdGuard

  1. Go to Settings → Encryption Settings
  2. Configure:
SettingValue
Enable Encryption☑ Checked
Server Nameexample.org (your domain)
HTTPS Port443 (DNS-over-HTTPS)
DNS-over-TLS Port853 (DNS-over-TLS)
DNS-over-QUIC Port853 (DNS-over-QUIC, uses UDP)
  1. Certificate & Key Paths (⚠️ Use file paths, NOT file contents):
    • Certificate path: /etc/letsencrypt/live/example.org/fullchain.pem
    • Private key path: /etc/letsencrypt/live/example.org/privkey.pem
  2. Click Save Settings
Why use paths instead of pasting contents? When Certbot renews your certificate (every 90 days), AdGuard will automatically use the new files.

Automate Certificate Renewal

Because AdGuard Home occupies port 80, Certbot needs AGH to be stopped before it can complete the HTTP-01 challenge. Create a pre and post hook to handle this automatically:
# Create pre-renewal hook (stops AGH before renewal)
sudo nano /etc/letsencrypt/renewal-hooks/pre/stop-adguard.sh
Paste this content:
#!/bin/bash
systemctl stop AdGuardHome
# Create post-renewal hook (starts AGH after renewal)
sudo nano /etc/letsencrypt/renewal-hooks/post/start-adguard.sh
Paste this content:
#!/bin/bash
systemctl start AdGuardHome
Make both scripts executable:
sudo chmod +x /etc/letsencrypt/renewal-hooks/pre/stop-adguard.sh
sudo chmod +x /etc/letsencrypt/renewal-hooks/post/start-adguard.sh
Verify everything works with a dry run:
sudo certbot renew --dry-run
Certbot automatically renews certificates. These hooks ensure AdGuard reloads the new certificate without manual intervention.

Update DigitalOcean Firewall Rules

  1. Delete the temporary delete-later rule (port 3000 is no longer needed)
    • In the DigitalOcean control panel, go to Networking → Firewalls, open your firewall, find the Custom TCP 3000 inbound rule, and remove it.
Security Note: The existing rules allow the entire internet to use your DNS server. Ensure you have strong authentication on the AdGuard web interface.

Setup Complete!

Your DNS server is now fully operational with:
  • Privacy: Direct recursive resolution via Unbound
  • Security: DNSSEC validation, encrypted protocols (DoH/DoT/DoQ)
  • Ad-blocking: Custom blocklists via AdGuard Home
  • Automation: Auto-renewing SSL certificates