Skip to content

Instantly share code, notes, and snippets.

@iGerman00
Last active March 8, 2026 23:26
Show Gist options
  • Select an option

  • Save iGerman00/5badd489429ee1d01e67d10540a41344 to your computer and use it in GitHub Desktop.

Select an option

Save iGerman00/5badd489429ee1d01e67d10540a41344 to your computer and use it in GitHub Desktop.
A simple tutorial on a private DNS server setup with Caddy and AdGuard Home

DIY Private Filtered DNS

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.

What you'll get

  • 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

Prerequisites

  1. A server (even a free Oracle Cloud instance is sufficient)
  2. A domain or subdomain pointed to your server
  3. Basic command line and Caddy comfort (or a friend who can help)

Step 1: Install required software

  1. Install Caddy web server (this tutorial assumes the default systemd installation)
  2. Install AdGuard Home using their Docker image (recommended)
  3. Make sure Docker and Docker Compose are installed

Step 2: Configure Docker for AdGuard Home

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

Step 3: Initial AdGuard Home setup

  1. Start Docker Compose:

    docker compose up -d
  2. Access the initial setup page at http://your-server-ip:9012

  3. Complete the setup wizard, creating an admin account and selecting your preferred filtering options

Step 4: Configure AdGuard Home

  1. 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
  2. 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

Step 5: Configure Caddy as a reverse proxy

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
    }
}

Step 6: Activate your configuration

  1. Reload Caddy to apply the configuration:

    sudo systemctl reload caddy
  2. Restart AdGuard Home:

    docker compose restart adguardhome

Step 7: Using your private DNS

On your devices, configure DNS-over-HTTPS with the following URL:

https://example.org/your_auth_token/dns-query

Where:

  • example.org is your domain
  • your_auth_token is the token you set in your Caddyfile
  • You can optionally add a device ID at the end: /dns-query/my-phone

Troubleshooting

  • 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!

@jamillejh
Copy link

How do you keep the certs in sync when caddy renews via let's encrypt so that adguard refreshes too? I'm using proxmox to host lxc for adguard and caddy and having problems sharing only adguard certs from caddy using host mapped folders for use by both lxc. How did you come over that?

@iGerman00
Copy link
Author

I don’t. I let Caddy handle certificates for external connections, but internally, it uses HTTP in the reverse proxy directive. I don’t believe Caddy is designed to share certificates, especially not between containers. Permissions become complex, and you’d be much better off using nginx for that kind of thing if you need certificates. However, do you really need them for server-to-self communication? For me, I simply ignore it, as long as my clients use DoH over proper HTTPS, which Caddy provides.

@iGerman00
Copy link
Author

How do you keep the certs in sync when caddy renews via let's encrypt so that adguard refreshes too? I'm using proxmox to host lxc for adguard and caddy and having problems sharing only adguard certs from caddy using host mapped folders for use by both lxc. How did you come over that?

Sorry, after reviewing my config, I do remember now what I did. My setup used to be insecure internally, but now:

I mounted the certs in my docker compose config:

 - /var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/example.org:/certs

Then pointed AdGuardHome.yml to them:

tls:
 enabled: true
 server_name: example.org
 force_https: false
 port_https: 443
 port_dns_over_tls: 0
 port_dns_over_quic: 0
 port_dnscrypt: 0
 dnscrypt_config_file: ""
 allow_unencrypted_doh: false
 certificate_chain: ""
 private_key: ""
 certificate_path: /certs/example.org.crt
 private_key_path: /certs/example.org.key

This lets caddy manage the certificates, but AdGuard Home will still be aware of them and can verify them independently of caddy.

@jamillejh
Copy link

jamillejh commented Mar 8, 2026

My primary use case was private dns on android which only supports DoH with fqdn not a url.

In the end I went for a slightly different approach for that, I compiled caddy with the event module to call a script which uses scp to copy the new certs over to adgaurd and adguard api to set paths and force a refresh to pick up the updated certs as I need them for lets encrypt cert updates.

I do like your implementation though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment