Deploying AdGuard with DNS-over-HTTPS in Docker

AdGuard provides centralized DNS filtering and encrypted upstream queries for infrastructure deployments. This guide covers deploying AdGuard in Docker with DNS-over-HTTPS upstreams, integrating it with Nginx Proxy Manager and WireGuard, and securing administrative access behind VPN boundaries.

Prerequisites

The following items are required:

  • Docker and Docker Compose installed on the host
  • Port 53 available on the host (configuration steps provided below)
  • Existing Docker networks for reverse proxy routing (backend-net) and VPN access
  • WireGuard VPN deployment (such as wg-easy) if remote access is required

Checking Port 53 Availability

Before deploying AdGuard Home, verify that port 53 is not already in use. Many modern Linux distributions run systemd-resolved by default, which binds to port 53 and will prevent AdGuard from starting.

Check if port 53 is currently bound:

sudo lsof -i :53

Alternatively, use ss to check for listeners on port 53:

sudo ss -tulpn | grep :53

If systemd-resolved or another service appears in the output, port 53 is occupied and must be freed before proceeding.

Resolving Port 53 Conflicts on Ubuntu and Debian

Ubuntu Server and recent Debian releases use systemd-resolved as the default DNS resolver. This service binds to 127.0.0.53:53 and acts as a local DNS stub resolver. There are two approaches to resolve this conflict.

Option 1: Disable systemd-resolved Completely

This approach stops systemd-resolved entirely and frees port 53 for AdGuard Home:

sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolved

After disabling systemd-resolved, /etc/resolv.conf must be reconfigured. Remove the existing symlink and create a new resolv.conf file:

sudo rm /etc/resolv.conf
sudo nano /etc/resolv.conf

Add the following content to use a temporary upstream DNS server until AdGuard is running:

nameserver 1.1.1.1
nameserver 8.8.8.8

After AdGuard is running, change this to:

nameserver 127.0.0.1

Option 2: Configure systemd-resolved to Not Bind Port 53

This approach keeps systemd-resolved running but prevents it from binding to port 53. Edit the resolved configuration file:

sudo nano /etc/systemd/resolved.conf

Add or modify the following lines under the [Resolve] section:

[Resolve]
DNS=127.0.0.1
DNSStubListener=no

The DNSStubListener=no directive prevents systemd-resolved from binding to port 53. Remove the existing resolv.conf symlink and create a new one pointing to the systemd-resolved runtime configuration:

sudo rm /etc/resolv.conf
sudo ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf

Restart systemd-resolved to apply the changes:

sudo systemctl restart systemd-resolved

Verify that port 53 is now free:

sudo lsof -i :53

The command should return no output if port 53 is available.

Network Architecture

AdGuard operates at three security boundaries:

Port 53 binds to the host interface to handle DNS queries from the local system and WireGuard clients. The web interface binds to internal Docker networks or 127.0.0.1 to prevent direct public exposure. External access to the admin panel occurs only through Nginx Proxy Manager with SSL and authentication enabled. WireGuard clients receive the AdGuard container IP as their DNS server, providing automatic ad-blocking and encrypted DNS lookups for all connected devices.

Directory Structure

Create persistent storage directories on the host:

mkdir -p /opt/docker/adguard/opt/adguardhome/work
mkdir -p /opt/docker/adguard/opt/adguardhome/conf

Create an environment file at /opt/docker/adguard/.env:

ROOT=/opt/docker
ADGUARD_WEB_PORT=8002

Docker Compose Configuration

Create /opt/docker/adguard/docker-compose.yml with the following configuration:

version: "3.8"

networks:
  backend-net:
    external: true

services:
  adguard:
    image: adguard/adguardhome:latest
    container_name: adguard
    volumes:
      - ${ROOT}/adguard/opt/adguardhome/work:/opt/adguardhome/work
      - ${ROOT}/adguard/opt/adguardhome/conf:/opt/adguardhome/conf
    networks:
      - backend-net
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "${ADGUARD_WEB_PORT}:80/tcp"
    healthcheck:
      test: ["CMD-SHELL", "netstat -pant | grep :53 || exit 1"]
      interval: 15s
      timeout: 30s
      retries: 3
    restart: unless-stopped

Initial Setup and DNS-over-HTTPS Configuration

Start the container:

cd /opt/docker/adguard && docker compose up -d

Verify the container started successfully:

docker logs adguard

If the container fails with a port binding error, return to the port 53 conflict resolution steps above.

Access the setup wizard at http://127.0.0.1:8002 through an SSH tunnel or local network connection. Configure the following settings during initial setup:

  • Set the admin web interface to listen on port 80 inside the container
  • Configure the DNS server to listen on 0.0.0.0 port 53
  • Create administrative credentials

Configure Encrypted Upstream Resolvers

After completing the initial setup, navigate to Settings then DNS Settings in the web interface. Remove any default plain-text resolvers such as 8.8.8.8 or 1.1.1.1. Under Upstream DNS servers, add encrypted DNS-over-HTTPS endpoints:

https://dns.cloudflare-dns.com/dns-query
https://dns.quad9.net/dns-query
https://doh.mullvad.net/dns-query

Set the upstream mode to Parallel requests to query all upstreams simultaneously and use the fastest response. Enable DNSSEC under DNS server configuration to prevent cache poisoning attacks. Click Test Upstreams to verify connectivity, then save the configuration.

Integration with Nginx Proxy Manager

To access the AdGuard admin interface through a secure domain without exposing port 8002 publicly, configure a proxy host in Nginx Proxy Manager:

  • Create a new Proxy Host
  • Set Forward Hostname to adguard (the container name on backend-net)
  • Set Forward Port to 80
  • Enable Block Common Exploits
  • Issue a Let’s Encrypt SSL certificate for the domain
  • Configure Access Lists to restrict access to local subnets or VPN IP ranges

Integration with WireGuard

Configure WireGuard to distribute the AdGuard DNS server to all VPN clients. In the wg-easy environment configuration, set:

WG_DEFAULT_DNS=10.0.0.42

Replace 10.0.0.42 with the IP address of the AdGuard container on the wg network or the host internal bridge IP. This setting forces all DNS queries from WireGuard clients through AdGuard Home.

Host DNS Configuration

To force the host system to use AdGuard for local DNS resolution, edit /etc/resolv.conf:

nameserver 127.0.0.1

On systems where systemd-resolved is still running with DNSStubListener disabled, the resolv.conf symlink should already point to 127.0.0.1 through the configuration applied earlier. Verify the contents:

cat /etc/resolv.conf

Verification

Test DNS resolution and ad-blocking from the host:

nslookup doubleclick.net 127.0.0.1

AdGuard should return 0.0.0.0 for blocked domains, confirming active filtering. Check the Query Log in the AdGuard web interface after performing a test lookup. The Upstream column should display the DNS-over-HTTPS URL such as https://dns.cloudflare-dns.com/dns-query, confirming that plain-text DNS queries are not leaving the host.

Verify that WireGuard clients receive the correct DNS server by checking the connection details on a connected device. All DNS queries from VPN clients should appear in the AdGuard query log.

AI assistance is used on this site for language, formatting, and turning research into a consistent template. It is not used to perform the underlying research or verify technical claims. Every command, configuration, and step in this post is tested by hand before publication.

Leave a Reply

Your email address will not be published. Required fields are marked *

eleven + five =