Multi-Instanced AdGuardHome Setup Guide

1919 words
10 minutes
Multi-Instanced AdGuardHome Setup Guide

Multi-Instance Setup with Adguard Home#

I use 2 MiniPCs for this setup - https://www.amazon.com/dp/B0C89TQ1YF

Install Ubuntu 24.04 (Or whatever you want ig) and install docker.

On the Main AdguardHome Server#

  • Volumes you can change if you don’t like the defaults.
  • Vector is optional, this is only if you want to export the data.
  • Under keepalived, you need to modify KEEPALIVED_VIRTUAL_IPS to the IP that will face the clients. (separate from the ips of the individual adguardhome servers).
  • KEEPALIVED_UNICAST_PEERS is the list of all IPs
  • Double check the interface. If you’re using standard ethernet w/ Ubuntu, enp1s0 should be the default, but just check with ip addr just to make sure.
Note

If you want to make it really minimal.

  • You can remove everything after adguardhome section if you’re running single instance.
  • Remove all labels if you’re not running WUD
  • Keepalived is for multi-instance setup / Vector is for vector log forwarding. You can remove the ones you don’t want.
services:
adguardhome:
image: adguard/adguardhome
container_name: adguardhome
restart: unless-stopped
volumes:
- '/opt/adguardhome/work:/opt/adguardhome/work'
- '/opt/adguardhome/conf:/opt/adguardhome/conf'
ports:
- '53:53/tcp'
- '53:53/udp'
# - '67:67/udp' # This is only if you need the dhcp server, which can conflict with some linux services.
# - '68:68/udp'
- '80:80/tcp'
- '443:443/tcp'
- '443:443/udp'
- '3000:3000/tcp'
- '853:853/tcp'
- '784:784/udp'
- '853:853/udp'
- '8853:8853/udp'
- '5443:5443/tcp'
- '5443:5443/udp'
labels:
- 'wud.tag.include=latest'
- 'wud.watch.digest=true'
vector:
image: timberio/vector:latest-alpine
restart: unless-stopped
command: --config /etc/vector/vector.toml
volumes:
- '/opt/adguardhome/work:/opt/adguardhome/work'
- '/opt/vector:/etc/vector'
labels:
- 'wud.tag.include=latest-alpine'
- 'wud.watch.digest=true'
keepalived:
image: lettore/keepalived
restart: unless-stopped
network_mode: "host"
cap_add:
- NET_ADMIN
- NET_BROADCAST
- NET_RAW
environment:
- KEEPALIVED_STATE=MASTER # Change this to BACKUP for subsequent instances
- KEEPALIVED_VIRTUAL_IPS="192.168.5.210"
- KEEPALIVED_UNICAST_PEERS="'192.168.5.211', '192.168.5.212', '192.168.5.213', '192.168.5.214', '192.168.5.215'"
- KEEPALIVED_INTERFACE=enp1s0
- KEEPALIVED_PASSWORD=adguard
- KEEPALIVED_PRIORITY=100 # Change this value to a lower value for backup instances
- KEEPALIVED_ROUTER_ID=5
labels:
- 'wud.tag.include=latest'
- 'wud.watch.digest=true'

Save as compose.yml in your home folder

docker compose up -d then docker compose logs -f to see the logs.

You can verify if it’s working by going to http://{KEEPALIVED_VIRTUAL_IPS}:3000 and it should show the adguard UI.

On the Secondary AdguardHome Server#

This AGH server will act as the “backup” so it’ll only be in use if you’re main is down.

  • It’s all the same except.
    • KEEPALIVED_STATE=BACKUP this needs to be set to BACKUP
services:
adguardhome:
image: adguard/adguardhome
container_name: adguardhome
restart: unless-stopped
volumes:
- '/opt/adguardhome/work:/opt/adguardhome/work'
- '/opt/adguardhome/conf:/opt/adguardhome/conf'
ports:
- '53:53/tcp'
- '53:53/udp'
# - '67:67/udp' # This is only if you need the dhcp server, which can conflict with some linux services.
# - '68:68/udp'
- '80:80/tcp'
- '443:443/tcp'
- '443:443/udp'
- '3000:3000/tcp'
- '853:853/tcp'
- '784:784/udp'
- '853:853/udp'
- '8853:8853/udp'
- '5443:5443/tcp'
- '5443:5443/udp'
labels:
- 'wud.tag.include=latest'
- 'wud.watch.digest=true'
vector:
image: timberio/vector:latest-alpine
restart: unless-stopped
command: --config /etc/vector/vector.toml
volumes:
- '/opt/adguardhome/work:/opt/adguardhome/work'
- '/opt/vector:/etc/vector'
labels:
- 'wud.tag.include=latest-alpine'
- 'wud.watch.digest=true'
keepalived:
image: lettore/keepalived
restart: unless-stopped
network_mode: "host"
cap_add:
- NET_ADMIN
- NET_BROADCAST
- NET_RAW
environment:
- KEEPALIVED_STATE=BACKUP # Change this to BACKUP for subsequent instances
- KEEPALIVED_VIRTUAL_IPS="192.168.5.210"
- KEEPALIVED_UNICAST_PEERS="'192.168.5.211', '192.168.5.212', '192.168.5.213', '192.168.5.214', '192.168.5.215'"
- KEEPALIVED_INTERFACE=enp1s0
- KEEPALIVED_PASSWORD=adguard
- KEEPALIVED_PRIORITY=200 # Change this value to a lower value for backup instances
- KEEPALIVED_ROUTER_ID=5
labels:
- 'wud.tag.include=latest'
- 'wud.watch.digest=true'

Save as compose.yml in your home folder

docker compose up -d then docker compose logs -f to see the logs.

Syncing the two instances together with AdGuardHome-Sync#

You’ll need to use some 3rd party to sync the two instances together.

I use the linuxserver/adguardhome-sync image

compose file probably looks something like this (not tested. just taken from my unraid instance)

services:
adguardhomesync:
image: lscr.io/linuxserver/adguardhome-sync
container_name: adguardhomesync
restart: unless-stopped
volumes:
- '/your_config_dir:/config'
ports:
- 8080:8080 # change this to whatever you want for the WebAPI
environment:
# For unraid users, but change it to whatever works for you.
- PUID=99
- PGID=100
- UMASK=022
- CONFIGFILE=/config/adguardhome-sync.yaml

You can either run this on your master instance, but I run it on a separate machine from the DNS servers.

Either append it to your already existing compose file (minus the services:) or just make a new compose file and you can run with docker compose -f filename.yml up -d

You’ll probably need to read the docs for an up-to-date for the config. Mine looks like this javascript

# cron expression to run in daemon mode. (default; "" = runs only once)
cron: "*/10 * * * *"
origin:
# url of the origin instance
url: http://{first_instance_ip}
# apiPath: define an api path if other than "/control"
# insecureSkipVerify: true # disable tls check
username: admin
password: password
# replicas instances (optional, if more than one)
replicas:
- url: http://{second_instance_ip}
username: adminSkip
password: password
# Configure the sync API server, disabled if api port is 0
api:
# Port, default 8080
port: 8080
# if username and password are defined, basic auth is applied to the sync API
username: username
password: password

Save as adguardhome-sync.yaml under the config dir you’re using.

vector.toml#

Note

you do not need this if you don’t use vector.

If you want to use vector to export data to like a influxdb, greptimedb, etc, this is what mine looks like

data_dir = "/etc/vector/data"
[sources.adguardfile]
type = "file"
include = ["/opt/adguardhome/work/data/querylog.json"]
[sinks.vector]
type = "vector"
inputs = ["adguardfile"]
address = "ADDRESS OF YOUR OTHER VECTOR INSTANCE RETRIEVING THIS DATA"

On the main vector server, i have this (you can adjust everything to make it fit to however you wan to setup

[sources.adguardhome]
type = "vector"
address = "0.0.0.0:9001"
[transforms.vector_to_json]
type = "remap"
inputs = ["adguardhome"]
source = """
. = parse_json!(string!(.message))
.T = replace!(.T, "Z", "+00:00")
.timestamp = parse_timestamp!(.T, "%FT%T%.9f%:z")
del(.T)
.IP_24 = parse_regex!(.IP, r'(?P<number>\\d+.\\d+.\\d+.?)').number + "x"
.TLD = parse_regex(.QH, r'^.*?(?P<tld>[^.]+.[^.]+)$').tld ?? ""
.Result = object(.Result) ?? {}
if length(.Result) != 0 {
.Reason = get(.Result, ["Reason"]) ?? null
} else {
.Reason = null
}
del(.OrigAnswer)
del(.Result)
"""
[sinks....] # You need some sort of sink to go to.

FAQ#

Why use keepalived? There’s Primary/Secondary DNS on my Router.#

  • https://superuser.com/questions/462926/how-do-preferred-and-alternate-or-multiple-dns-servers-work
  • Basically, the behavior of that “primary/secondary” is completely up to the client.
    • Router sends the primary/secondary information to the client, but what the client does with that information is totally up to the client and a lot of times aren’t properly implemented.
    • When it decides to failover, when it decides to failback, when it decides to even use the secondary DNS is up to the client. And this varies a lot. For example, a lot of TVs don’t even bother using the secondary. Some clients don’t even store the secondary DNS IP. Windows (at least from when I tried) doesn’t even failover within a reasonable amount of time if at all, and the failback is unpredictable.
  • Keepalived will properly tell the router whether to use the primary/secondary based on whether they’re able to ping each other or not.

What to set as my Virtual/Machine IPs?#

  • It is better to assign these from your router. Set the MAC Address to be statically assigned an IP, rather than trying to set this up from the machine’s side. It’s way more easily maintainable this way.
  • You’ll also want to make sure your DHCP server does not assign IPs in the range of static assignments. Most routers these days should be smart enough to not do that, but I would just do it just in case.
    • For example. My DHCP server will assign 192.168.5.10 - 192.168.5.200. It will not assign anything in that 192.168.5.201 -192.168.5.255 which I can use for static assignments.

Why use docker?#

  • Docker will isolate the environment that AGH runs and remove any dynamic dependencies between AGH and the system. The only time things might break is if Docker has some issue, but that is way smaller of a blast radius than installing it directly.
  • It’s way easier to deploy. Once you have your compose file, you can just repeatedly deploy pretty easily.
    • If you use something like Arcane/Portainer, you can use GitOps feature to then sync with a git repo (although you’d probably want to either make the IPs more dynamically configured for privacy or use a private/onprem repo).

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.51/images/lettore/keepalived/json": dial unix /var/run/docker.sock: connect: permission denied#

What filters do you use? (Filters → DNS blocklists)#

Basic Adguard Guide#

  • Basically the main thing is filters → Custom filtering rules
    • Do @@||{domain_name}^$important if you want to whitelist something
    • Do ||{domain_name} to block a new thing. 

DoH/DoT#

  • My setup is the following
    • Upstream DNS Servers set to https://dns.cloudflare.com/dns-query
    • Load-Balancing
    • Bootstrap DNS servers is set to 1.1.1.1 and 1.0.0.1 (without this, your dns will not work when coming back online unless you have a DNS leak somewhere.
    • Private reverse DNS servers set to your router’s address.
    • Use Private reverse DNS resolvers - True
    • Enable reverse resolving of clients IP addresses - True
  • /etc/resolv.conf (not inside docker, but on the host) looks like
Terminal window
domain lan
search lan
nameserver {KEEPALIVED_VIRTUAL_IPS}

AdGuardHomeStats
AdGuardHomeStats

Some clients are not using AdguardDNS#

  • This is probably due to either 1) the client hardcoded the DNS (common with a lot of TVs/IoT in particular) or 2) it hasn’t refreshed DHCP yet, so it hasn’t gotten the new DNS IPs from your router.
  • Solving the Hardcoded DNS problem.
    • Best way is to redirect all outgoing requests to port 53 to force them to go to your adguard instance.

    • You’ll need to figure this out with your own router, but here’s my OPNSense configuration

      OPNSense
      OPNSense

Load-Balancing servers rather than Primary/Backup#

  • Probably have to use something like HAProxy

DNS Certificate / Using a domain for adguard / Setting custom domain to resolve locally when on local network.#

  • TBD if there’s interest.

Auto-Updating Everything#

  • To auto-update the apt sequence, I don’t have a good solution yet. Most simple is probably just some cron job or smth. I think there’s an auto updater for apt now idk

  • To auto-update the containers, I’m currently using Arcane which is similar to portainer, which has an auto-update feature.

  • Can also use something like WUD. Don’t use watchtower that’s long gone.

  • Alternatively…. you can update manually yourself

    Terminal window
    sudo apt update && sudo apt upgrade -y
    sudo docker compose pull && sudo docker compose down && sudo docker compose up -d

Want to try my new vibe-coded project?#

https://github.com/NekoShinobi/blocked-agh

Basically a way to check if an address is blocked or not, and it doesn’t require authentication. And the user can then request to have it unblocked.

My experience with AGH vs PiHole (old. back when i migrated)#

  • So far, I do like AGH a lot more for my simplistic use cases. Having DoH/DoT supported out of the box is a billion times more convenient than having to setup all these complicated workarounds to make DoH work with each provider. Even if I provided a solution for Cloudflare, it would not necessarily translate to using a different DoH network.
  • PiHole’s UI has come a long way and had a lot of improvements over the past year. However, it’s still very much lacking from AGH’s fast and responsive UI.
  • AGH is missing client groups and an easier way to manage groups of users rather than having to manually enter one at a time. This is not really something I personally utilize, so it’s not a dealbreaker for me.
  • AGH’s way of handling wildcard DNS Rewrites is so much better than PiHole’s manual entry. It’s so annoying and unclear on how a not-so-experienced user does this on PiHole.
  • AGH has a weird UX with allow/blocklists. It has way too many modals and loading screens just to do a simple entry for a list. I wish this was just one page that handled it, and allowed a way to edit multiple entries at once like how PiHole did it.
  • AGH’s approach to custom filtering lists is something PiHole does a lot better. AGH approaches it as a product where they expect the user to be already familiar with the style of how AdGuard or uBlock creates custom lists. However, they really should have this as an advanced feature, and have a more user-friendly version of this as the front page.

Share Article

If this article helped you, please share it with others!

Multi-Instanced AdGuardHome Setup Guide
https://dahn.me/posts/adguardhome/
Author
Daniel Ahn
Published at
2026-01-27
License
CC BY-NC-SA 4.0
Profile Image of the Author
Daniel Ahn
いい元気だね、何かいいことでもあったのかい?
Announcement
This page is a work-in-progress. Thanks for checking it out!
Music
Cover

Music

No playing

0:00 0:00
No lyrics available
Categories
Tags
Site Statistics
Posts
5
Categories
2
Tags
18
Total Words
12,113
Running Days
0 days
Last Activity
0 days ago

Table of Contents