Create your own secure DNS server with filtering capabilities
NextDNS, eat your heart out
This tutorial will guide you through setting up a private DNS server using Caddy and AdGuard Home. You'll create a secure, encrypted personal DNS endpoint with content filtering and authorization that you can use from anywhere in the world.
- A personal DNS server that blocks ads and unwanted content
- Encrypted DNS connections for privacy
- Access from any modern device that supports DNS-over-HTTPS (DoH)
- Authentication to prevent unauthorized access
- A server (even a free Oracle Cloud instance is sufficient)
- A domain or subdomain pointed to your server
- Basic command line and Caddy comfort (or a friend who can help)
- Install Caddy web server (this tutorial assumes the default systemd installation)
- Install AdGuard Home using their Docker image (recommended)
- Make sure Docker and Docker Compose are installed
Create a docker-compose.yml
file with the following content:
version: "3.3"
services:
adguardhome:
container_name: adguardhome
restart: unless-stopped
volumes:
- ./work:/opt/adguardhome/work
- ./conf:/opt/adguardhome/conf
- /var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/example.org:/certs
# ⚠️ IMPORTANT! Replace "example.org" with your actual domain
# Make sure this directory exists and contains .crt and .key files
ports:
- 1234:80/tcp # Dashboard access
- 5678:443/tcp # DNS over HTTPS
- 5678:443/udp # DNS over HTTPS
- 9012:3000/tcp # Initial configuration page
image: adguard/adguardhome
-
Start Docker Compose:
docker compose up -d
-
Access the initial setup page at
http://your-server-ip:9012
-
Complete the setup wizard, creating an admin account and selecting your preferred filtering options
-
Edit the
conf/AdGuardHome.yaml
file to add trusted proxies (for correct client IP display):dns: trusted_proxies: - 172.16.0.0/12 # Add this line for Docker subnet - 127.0.0.0/8 - ::1/128
-
In the AdGuard Home dashboard, configure encryption settings:
- Set server name to your domain (e.g.,
example.org
) - Set the certificate paths to:
/certs/example.org.crt
/certs/example.org.key
- You can keep the default HTTPS port (443) or change it (update your Docker Compose file if you do)
- Clear any DNS-over-TLS and QUIC port settings if present
- Save the settings
- Set server name to your domain (e.g.,
Create or edit your Caddyfile:
https://example.org {
# DNS-over-HTTPS format: example.org/your_auth_token/dns-query/[optional_device_id]
# Example: https://example.org/qwerty1234/dns-query/my-iphone
vars {
# Generate a secure token with: openssl rand -hex 32
auth_token 1611709b3d87afec72b914e8c95e26d3644419d62687567e274ade41456afb02
}
@auth_token path /{http.vars.auth_token}*
handle @auth_token {
uri strip_prefix /{http.vars.auth_token}
handle /dns-query* {
reverse_proxy https://127.0.0.1:5678 {
transport http {
tls_insecure_skip_verify
}
# For proper client IP tracking:
header_up Host {upstream_hostport}
header_up X-Real-IP {http.request.remote.host}
}
}
handle {
# Requests with valid token but invalid path
respond "Invalid request" 400
}
}
handle {
# Unauthorized requests (including homepage)
respond "Hello." 403
}
}
-
Reload Caddy to apply the configuration:
sudo systemctl reload caddy
-
Restart AdGuard Home:
docker compose restart adguardhome
On your devices, configure DNS-over-HTTPS with the following URL:
https://example.org/your_auth_token/dns-query
Where:
example.org
is your domainyour_auth_token
is the token you set in your Caddyfile- You can optionally add a device ID at the end:
/dns-query/my-phone
- If AdGuard can't access the certificates, check the folder permissions. I run such smaller stuff with Dockge, which runs containers as root
- If DNS isn't working, verify the ports in your Docker Compose file match the ones in your Caddyfile
- Check your domain's DNS settings to make sure it points directly to your server
Now you have your own private, secure, and filtered DNS service that you control completely!