A comprehensive guide to deploy AdGuard Home with full DNS encryption (DoH + DoT) behind a reverse proxy, with no plain DNS exposed.
- Overview
- Features
- Architecture
- Prerequisites
- Quick Start
- Detailed Setup Guide
- Reverse Proxy Configuration
- Client Configuration
- UniFi DNS Shield Setup
- Security Hardening
- Maintenance
- Troubleshooting
- Thanks to Open Source
- Contributing
- License
This guide walks you through deploying a fully encrypted DNS server using AdGuard Home. By the end, you'll have:
- DNS-over-HTTPS (DoH) on port 443
- DNS-over-TLS (DoT) on port 853
- Plain DNS completely disabled
- Valid Let's Encrypt SSL certificates
- Automatic certificate renewal
- Ad-blocking and privacy protection
| Benefit | Description |
|---|---|
| Privacy | Your ISP cannot see your DNS queries |
| Security | All DNS traffic is encrypted end-to-end |
| Ad Blocking | Network-wide ad and tracker blocking |
| Control | Full control over DNS filtering rules |
| No Logging | No third-party logging your queries |
- β Full Encryption - No plain DNS (port 53) exposed
- β DoH Support - DNS-over-HTTPS on port 443
- β DoT Support - DNS-over-TLS on port 853
- β Valid SSL - Let's Encrypt certificates with auto-renewal
- β Docker Deployment - Easy to deploy and maintain
- β Reverse Proxy Ready - Works behind Nginx, Traefik, Caddy, or Pangolin
- β Ad Blocking - Built-in ad and tracker blocking
- β Web Dashboard - Easy-to-use admin interface
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ENCRYPTED DNS ARCHITECTURE β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Client ββDoH/DoTβββΆ Reverse Proxy ββHTTPSβββΆ AdGuard ββDoHβββΆ Upstream β
β π π Home π DNS β
β (No Plain DNS) β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
All traffic encrypted β
Valid SSL β
No port 53 exposed β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
flowchart TB
subgraph Internet["βοΈ Public Internet"]
Client["π₯οΈ Client Device"]
end
subgraph ReverseProxy["Reverse Proxy Server"]
Proxy["π Nginx/Traefik/Caddy/Pangolin<br/>TLS Termination<br/>Let's Encrypt Cert"]
end
subgraph DNSServer["DNS Server"]
subgraph Docker["π³ Docker"]
AGH["π‘οΈ AdGuard Home<br/>Port 443 (DoH)<br/>Port 853 (DoT)<br/>Plain DNS: DISABLED"]
end
end
subgraph Upstream["βοΈ Upstream DNS"]
Cloudflare["π Cloudflare<br/>1.1.1.1 (DoH)"]
Google["π Google<br/>8.8.8.8 (DoH)"]
Quad9["π Quad9<br/>9.9.9.9 (DoH)"]
end
Client -->|"π DoH :443"| Proxy
Proxy -->|"π HTTPS"| AGH
AGH -->|"π DoH"| Cloudflare
AGH -->|"π DoH"| Google
AGH -->|"π DoH"| Quad9
| Port | Protocol | Purpose | Status |
|---|---|---|---|
| 443 | HTTPS | DNS-over-HTTPS + Web UI | β Enabled |
| 853 | TLS | DNS-over-TLS | β Enabled |
| 80 | HTTP | Web UI (internal only) | β Internal |
| 53 | DNS | Plain DNS | β Disabled |
Before starting, ensure you have:
- A VPS or server with a public IP (or behind a reverse proxy)
- A domain name with DNS access (for SSL certificates)
- Docker and Docker Compose installed
- Basic terminal/SSH knowledge
| Provider | Use Case |
|---|---|
| Hetzner | Great EU pricing, good performance |
| DigitalOcean | Simple setup, good documentation |
| Vultr | Global locations, hourly billing |
| Linode | Reliable, good support |
| Provider | DNS Challenge Support |
|---|---|
| Cloudflare | β Recommended |
| Route53 | β Supported |
| DigitalOcean | β Supported |
| Google Cloud DNS | β Supported |
For experienced users, here's the quick setup:
# 1. Create directories
mkdir -p ~/adguard-home/{work,conf,certs}
cd ~/adguard-home
# 2. Get SSL certificates (using Cloudflare DNS challenge)
sudo certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \
-d dns.yourdomain.com
# 3. Copy certificates
sudo cp /etc/letsencrypt/live/dns.yourdomain.com/fullchain.pem ~/adguard-home/certs/
sudo cp /etc/letsencrypt/live/dns.yourdomain.com/privkey.pem ~/adguard-home/certs/
chmod 600 ~/adguard-home/certs/*
# 4. Create docker-compose.yml (see below)
# 5. Start container
docker compose up -d
# 6. Complete setup wizard at http://localhost:3000
# 7. Enable encryption in Settings β Encryption
# 8. Disable plain DNSsudo apt update && sudo apt upgrade -y# Install Docker
curl -fsSL https://get.docker.com | sh
# Add your user to docker group
sudo usermod -aG docker $USER
# Logout and login again, then verify
docker --version
docker compose versionmkdir -p ~/adguard-home/{work,conf,certs}
cd ~/adguard-homeDirectory purposes:
| Directory | Purpose |
|---|---|
work/ |
Runtime data, query logs, statistics |
conf/ |
Configuration files (AdGuardHome.yaml) |
certs/ |
SSL certificates |
You need valid SSL certificates for DoH/DoT to work. We'll use Let's Encrypt with Cloudflare DNS challenge.
sudo apt install -y certbot python3-certbot-dns-cloudflare- Go to Cloudflare Dashboard β API Tokens
- Click "Create Token"
- Use template "Edit zone DNS"
- Configure:
- Permissions:
Zone - DNS - Edit - Zone Resources:
Include - Specific zone - yourdomain.com
- Permissions:
- Create and copy the token (won't be shown again)
mkdir -p ~/.secrets/certbot
nano ~/.secrets/certbot/cloudflare.iniContent (single line only):
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN_HERE
β οΈ Important: ReplaceYOUR_CLOUDFLARE_API_TOKEN_HEREwith your actual token. No quotes!
Set secure permissions:
chmod 600 ~/.secrets/certbot/cloudflare.inisudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \
--dns-cloudflare-propagation-seconds 60 \
-d dns.yourdomain.com \
--agree-tos \
--email your-email@example.com \
--non-interactiveVerify:
sudo certbot certificates# Copy to AdGuard directory
sudo cp /etc/letsencrypt/live/dns.yourdomain.com/fullchain.pem ~/adguard-home/certs/
sudo cp /etc/letsencrypt/live/dns.yourdomain.com/privkey.pem ~/adguard-home/certs/
# Set ownership and permissions
sudo chown $USER:$USER ~/adguard-home/certs/*
chmod 600 ~/adguard-home/certs/fullchain.pem
chmod 600 ~/adguard-home/certs/privkey.pemnano ~/adguard-home/docker-compose.ymlInitial Setup Configuration:
services:
adguardhome:
image: adguard/adguardhome:latest
container_name: adguardhome
restart: unless-stopped
volumes:
- ./work:/opt/adguardhome/work
- ./conf:/opt/adguardhome/conf
- ./certs:/opt/adguardhome/certs:ro
ports:
# HTTPS + DoH
- "443:443/tcp"
# DNS-over-TLS
- "853:853/tcp"
# Web UI (local access)
- "127.0.0.1:8080:80/tcp"
# Initial setup (remove after setup)
- "127.0.0.1:3000:3000/tcp"π‘ Note: If you're behind a reverse proxy or on a private network, bind to your private IP instead of
0.0.0.0.
Example for private network (e.g., 10.0.0.5):
ports:
- "10.0.0.5:443:443/tcp"
- "10.0.0.5:853:853/tcp"
- "127.0.0.1:8080:80/tcp"
- "127.0.0.1:3000:3000/tcp"cd ~/adguard-home
docker compose pull
docker compose up -dVerify:
docker ps | grep adguardhome
docker logs adguardhomeOption A: Direct access (if ports are public)
Open http://YOUR_SERVER_IP:3000
Option B: SSH tunnel (recommended for security)
# On your local machine
ssh -L 3000:localhost:3000 user@your-server-ipThen open http://localhost:3000
- Welcome: Click "Get Started"
- Admin Web Interface:
- Listen interface:
All interfaces - Port:
80
- Listen interface:
- DNS Server:
- Listen interface:
All interfaces - Port:
53
- Listen interface:
- Authentication:
- Create strong username and password
- Configure devices: Skip for now
- Open Dashboard
This is the most important step for full encryption.
Navigate to: Settings β Encryption settings
| Setting | Value |
|---|---|
| Enable encryption | β Checked |
| Enable plain DNS | β Keep checked (for now) |
| Server name | dns.yourdomain.com |
| Redirect to HTTPS automatically | β Checked |
| HTTPS port | 443 |
| DNS-over-TLS port | 853 |
| DNS-over-QUIC port | (leave empty) |
| Setting | Value |
|---|---|
| Set a certificates file path | β Selected |
| Certificates file path | /opt/adguardhome/certs/fullchain.pem |
| Private key file path | /opt/adguardhome/certs/privkey.pem |
Click "Save configuration"
You should see:
- β Certificate chain is valid
- β Subject: CN=dns.yourdomain.com
- β Issuer: Let's Encrypt
- β Valid private key
β οΈ Important: You must have DNS-over-TLS port (853) configured before disabling plain DNS!
In Settings β Encryption settings:
- Verify DNS-over-TLS port is
853 - Uncheck "Enable plain DNS"
- Click "Save configuration"
If you get an error: Ensure DNS-over-TLS port 853 is set first.
docker restart adguardhome# Plain DNS should NOT be listening
sudo ss -tlnp | grep :53
# Should return nothing
# DoH and DoT should be listening
sudo ss -tlnp | grep -E '(:443|:853)'Set encrypted upstream DNS servers for maximum privacy.
Navigate to: Settings β DNS settings
Upstream DNS servers:
https://1.1.1.1/dns-query
https://1.0.0.1/dns-query
https://dns.google/dns-query
https://dns.quad9.net/dns-query
Bootstrap DNS servers:
1.1.1.1
8.8.8.8
9.9.9.9
Click "Apply"
nano ~/adguard-home/conf/AdGuardHome.yamlFind the dns: section:
dns:
upstream_dns:
- https://1.1.1.1/dns-query
- https://1.0.0.1/dns-query
bootstrap_dns:
- 1.1.1.1
- 8.8.8.8sudo nano /etc/letsencrypt/renewal-hooks/deploy/adguard-certs.shContent:
#!/bin/bash
# Configuration - UPDATE THESE VALUES
DOMAIN="dns.yourdomain.com"
ADGUARD_USER="your-username"
ADGUARD_CERTS="/home/${ADGUARD_USER}/adguard-home/certs"
LOGFILE="/var/log/adguard-cert-renewal.log"
echo "$(date): Starting certificate deployment for ${DOMAIN}" >> $LOGFILE
# Copy new certificates
cp /etc/letsencrypt/live/${DOMAIN}/fullchain.pem ${ADGUARD_CERTS}/
cp /etc/letsencrypt/live/${DOMAIN}/privkey.pem ${ADGUARD_CERTS}/
# Set ownership and permissions
chown ${ADGUARD_USER}:${ADGUARD_USER} ${ADGUARD_CERTS}/*
chmod 600 ${ADGUARD_CERTS}/*
# Restart AdGuard Home
docker restart adguardhome
echo "$(date): Certificate deployment completed" >> $LOGFILE
β οΈ UpdateDOMAINandADGUARD_USERwith your values!
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/adguard-certs.shsudo certbot renew --dry-runRemove the setup port:
services:
adguardhome:
image: adguard/adguardhome:latest
container_name: adguardhome
restart: unless-stopped
volumes:
- ./work:/opt/adguardhome/work
- ./conf:/opt/adguardhome/conf
- ./certs:/opt/adguardhome/certs:ro
ports:
# HTTPS + DoH
- "443:443/tcp"
# DNS-over-TLS
- "853:853/tcp"
# Web UI (local/tunnel access only)
- "127.0.0.1:8080:80/tcp"cd ~/adguard-home
docker compose down
docker compose up -dIf you're running AdGuard Home behind a reverse proxy, here are configurations for popular options.
server {
listen 443 ssl http2;
server_name dns.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/dns.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dns.yourdomain.com/privkey.pem;
location / {
proxy_pass https://127.0.0.1:443;
proxy_ssl_verify off;
proxy_ssl_server_name on;
proxy_ssl_name dns.yourdomain.com;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}labels:
- "traefik.enable=true"
- "traefik.http.routers.adguard.rule=Host(`dns.yourdomain.com`)"
- "traefik.http.routers.adguard.entrypoints=https"
- "traefik.http.routers.adguard.tls=true"
- "traefik.http.routers.adguard.tls.certresolver=letsencrypt"
- "traefik.http.services.adguard.loadbalancer.server.port=443"
- "traefik.http.services.adguard.loadbalancer.server.scheme=https"dns.yourdomain.com {
reverse_proxy https://127.0.0.1:443 {
transport http {
tls_insecure_skip_verify
tls_server_name dns.yourdomain.com
}
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Proto {scheme}
}
}| Setting | Value |
|---|---|
| Domain | dns.yourdomain.com |
| Target | YOUR_SERVER_IP:443 |
| Protocol | https |
| Enable SSL to target | β Yes |
| TLS Server Name (SNI) | dns.yourdomain.com |
https://dns.yourdomain.com/dns-query
- Settings β Privacy & Security
- Scroll to "DNS over HTTPS"
- Select "Custom"
- Enter:
https://dns.yourdomain.com/dns-query
- Settings β Privacy and security β Security
- Enable "Use secure DNS"
- Select "Custom"
- Enter:
https://dns.yourdomain.com/dns-query
- Settings β Network & internet β Private DNS
- Select "Private DNS provider hostname"
- Enter:
dns.yourdomain.com
Use a DNS profile generator:
Or create a .mobileconfig profile manually.
DoH uses binary format (RFC 8484):
# Create DNS query
printf '\x00\x00\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\x03com\x00\x00\x01\x00\x01' > /tmp/dns.bin
# Test endpoint
curl -s -X POST "https://dns.yourdomain.com/dns-query" \
-H "content-type: application/dns-message" \
--data-binary @/tmp/dns.bin | xxd | head -5Note: JSON format (
?name=example.com&type=A) may return "Bad Request" - this is normal. Standard clients use binary format.
If you're using UniFi network equipment, you can configure DNS Shield to use your DoH server.
Go to: https://dnscrypt.info/stamps/
| Field | Value |
|---|---|
| Protocol | DNS-over-HTTPS |
| Host name | dns.yourdomain.com |
| Path | /dns-query |
| No logs | β Checked |
Copy the generated sdns://... stamp.
- UniFi Network Console β Settings β Security β DNS Shield
- Enable DNS Shield
- Select Custom
- Enter:
- Server Name:
yourdomain-dns(no dots allowed) - DNS Stamp:
sdns://...(paste your stamp)
- Server Name:
- Apply Changes
Prevent devices from bypassing your DNS:
- Settings β Security β Traffic & Firewall Rules
- Create rule:
- Action: Block
- Type: App
- App: DNS over HTTPS, DNS over TLS
- Source: Your networks
# Allow only necessary traffic
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 443/tcp # DoH
sudo ufw allow 853/tcp # DoT
sudo ufw enableIf using a reverse proxy like Pangolin, you can restrict access:
| Priority | Action | Match Type | Value |
|---|---|---|---|
| 1 | Allow | IP | Your home IP |
| 2 | Allow | IP | Your office IP |
| 3 | Block | IP Range | 0.0.0.0/0 |
If behind a reverse proxy, add to AdGuardHome.yaml:
trusted_proxies:
- 127.0.0.0/8
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16docker logs -f adguardhomecd ~/adguard-home
docker compose pull
docker compose up -dtar -czvf ~/adguard-backup-$(date +%Y%m%d).tar.gz ~/adguard-home/conf/tar -xzvf ~/adguard-backup-YYYYMMDD.tar.gz -C ~/
docker restart adguardhomesudo certbot certificatessudo certbot renew --force-renewal# Via SSH tunnel
ssh -L 8080:localhost:8080 user@your-server
# Then open http://localhost:8080| Issue | Cause | Solution |
|---|---|---|
| Certbot parsing error | Wrong cloudflare.ini format | Single line: dns_cloudflare_api_token = TOKEN (no quotes) |
| DNS challenge fails | Propagation too slow | Increase --dns-cloudflare-propagation-seconds to 120 |
| Certificate permission denied | Wrong file permissions | chmod 600 on both cert files |
| "Disabling plain DNS requires encrypted protocol" | DoT port not set | Set DNS-over-TLS port to 853 first |
| DoH returns "Bad Request" | JSON format not supported | Use binary format - this is normal |
| Can't access web UI | Port not exposed | Add 127.0.0.1:8080:80 to docker-compose |
# Check container status
docker ps | grep adguardhome
# View logs
docker logs adguardhome 2>&1 | tail -50
# Check listening ports
sudo ss -tlnp | grep -E '(:443|:853|:8080)'
# Test certificate
openssl x509 -in ~/adguard-home/certs/fullchain.pem -noout -text | head -30
# Test HTTPS locally
curl -k -v https://localhost:443| Log | Location |
|---|---|
| AdGuard Home | docker logs adguardhome |
| Certbot | /var/log/letsencrypt/letsencrypt.log |
| Renewal Hook | /var/log/adguard-cert-renewal.log |
Before going live, verify:
- Plain DNS (port 53) is disabled
- DoH (port 443) is working
- DoT (port 853) is working
- Valid SSL certificates installed
- Certificate auto-renewal configured
- Strong admin password set
- Firewall configured
- Web UI only accessible via SSH tunnel or whitelist
- Upstream DNS uses encrypted protocols
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
This guide is released under the MIT License. See LICENSE for details.
This guide and setup would not be possible without the incredible work of these open source projects and organizations. We are deeply grateful for their contributions to the community.
AdGuard Home is a network-wide software for blocking ads and tracking. It operates as a DNS server that re-routes tracking domains to a "black hole", thus preventing your devices from connecting to those servers.
| GitHub | github.com/AdguardTeam/AdGuardHome |
| Website | adguard.com/adguard-home |
| License | GPL-3.0 |
| Stars |
Why we love it:
- Free and open source
- Network-wide ad blocking
- DNS-over-HTTPS, DNS-over-TLS, DNS-over-QUIC support
- Beautiful web interface
- Active development and community
- Privacy-focused
Pangolin is a self-hosted tunneled reverse proxy with identity-aware access control, designed to securely expose private resources through encrypted tunnels without opening ports on your firewall.
| GitHub | github.com/fosrl/pangolin |
| Website | pangolin.dev |
| License | MIT |
| Stars |
Why we love it:
- Self-hosted Cloudflare Tunnel alternative
- No need to open ports on your firewall
- Built-in identity-aware access control
- IP whitelisting and authentication
- Beautiful dashboard
- Active development
Docker is a platform for developing, shipping, and running applications in containers. It enables developers to package applications with all dependencies into standardized units.
| GitHub | github.com/docker |
| Website | docker.com |
| License | Apache-2.0 |
Why we love it:
- Simplifies deployment
- Consistent environments
- Easy updates and rollbacks
- Huge ecosystem
- Industry standard
Let's Encrypt is a free, automated, and open Certificate Authority, run for the public's benefit by the Internet Security Research Group (ISRG).
| GitHub | github.com/letsencrypt |
| Website | letsencrypt.org |
| Operated by | Internet Security Research Group (ISRG) |
Why we love it:
- Free SSL certificates for everyone
- Automated certificate issuance and renewal
- Made HTTPS accessible to all
- Non-profit organization
- Secured billions of websites
Cloudflare provides content delivery network services, cloud cybersecurity, DDoS mitigation, and ICANN-accredited domain registration services.
| Website | cloudflare.com |
| DNS | 1.1.1.1 |
Why we love it:
- Free DNS with privacy focus (1.1.1.1)
- DNS-over-HTTPS support
- Excellent API for automation
- Free tier for personal projects
- Transparent security practices
| Project | Description | Link |
|---|---|---|
| Certbot | EFF's tool for obtaining Let's Encrypt certificates | certbot.eff.org |
| Traefik | Modern reverse proxy and load balancer | traefik.io |
| Nginx | High-performance web server and reverse proxy | nginx.org |
| Caddy | Web server with automatic HTTPS | caddyserver.com |
Open source software is the backbone of the modern internet. These projects, maintained by dedicated individuals and organizations, often without direct compensation, make secure and private internet access possible for everyone.
If you benefit from these projects, consider:
- β Starring their repositories on GitHub
- π° Donating to support development
- π Reporting bugs and issues
- π Contributing documentation or code
- π£ Spreading the word about their projects
"Open source is not about code. It's about people."
If this guide helped you, consider:
- β Starring the repository
- π Reporting issues
- π Improving documentation
- π Submitting pull requests
Happy secure DNS browsing! π