Running AdGuard Home in Docker on Synology
AdGuard Home runs here as a Docker container on Synology, managed through Container Manager
(Synology's rebrand of the Docker GUI): a normal docker-compose.yml, versioned like
everything else in the homelab. Three things about this setup aren't obvious until they bite:
the setup port that stops working on purpose, the port that's almost always already taken, and
the networking mode that decides whether AdGuard can see who's actually asking it a question.
Summary
- Gotcha #1: port 53 is almost always already taken on a Synology by something else.
- Gotcha #2: port 3000 keeps working after setup unless you point the web UI elsewhere during the wizard. It doesn't change on its own.
- Gotcha #3: bridge networking hides real client IPs from AdGuard's per-client stats.
- Fix: the working compose file below, plus what to check before you assume it's broken.
The compose file
services:
adguardhome:
image: adguard/adguardhome
container_name: adguardhome
restart: unless-stopped
volumes:
- /volume1/docker/adguardhome/work:/opt/adguardhome/work
- /volume1/docker/adguardhome/conf:/opt/adguardhome/conf
ports:
- "53:53/tcp"
- "53:53/udp"
- "3000:3000/tcp" # setup wizard, and the ongoing admin UI if you keep it on this port
Adjust /volume1/docker/adguardhome to wherever you keep container data on your NAS.
/volume1/docker/<app> is the common Synology convention, but any path works as long as it
persists across container restarts. work holds the query log and stats; conf holds
AdGuardHome.yaml, the actual config.
Port 3000 is the setup wizard: after that, it depends what you picked
Port 3000 runs the initial setup wizard. The first time you open http://<your-nas-ip>:3000,
AdGuard Home walks you through an admin username/password and two separate port choices: which
port the DNS server listens on (normally 53, see below) and which port the web admin
UI listens on going forward. There's no requirement to change that second one: keeping it on
3000 is completely normal, and that's what I run here.
If you do point the web UI at a different port during setup, 3000 stops responding once the
wizard finishes and the UI moves to whatever you chose instead. Either path is fine; the mistake
is assuming the port always changes, bookmarking :3000, and then being confused later about
why it's still working (or why it suddenly isn't). It depends entirely on what you picked on
that one wizard screen, not on any AdGuard default you have to fight.
Whichever port you pick, it has to already be published in your compose file. Docker doesn't
expose a port just because AdGuard is listening on it internally. If you move the UI to 80
during setup but only mapped 3000:3000 under ports:, the container is serving on 80 inside
the container with nothing on the host pointing at it. Add the port before you run the wizard,
or add it and redeploy right after. Either order works, but the mapping has to exist before you
can actually reach it from outside the container.
Port 53 is very likely already taken
This is the gotcha that actually blocks people, not port 3000. Synology reserves port 53 for its own purposes more often than people expect: either the DNS Server package if it's installed, or DSM's own internal resolver process even when it isn't. Either way, the container fails to bind, or worse, appears to start but never actually answers queries.
Check what's holding port 53 before you assume the compose file is wrong:
sudo netstat -tulpn | grep :53
If something's there, you have two options: stop/uninstall whatever DSM service owns it (Package Center → DNS Server, if that's installed), or run AdGuard on a different host entirely. There's no way to share port 53 between two listeners on the same IP.
Host networking vs bridge: pick your tradeoff
AdGuard Home wants to see the real IP address of whoever's asking it a question. That's how its per-client stats, filtering rules and query log attribution work. Docker's default bridge network puts a NAT hop between the container and the outside world, so every query looks like it came from the Docker gateway IP instead of the actual client.
Two honest options, neither universally "correct":
network_mode: host, simplest, and it works:
services:
adguardhome:
image: adguard/adguardhome
container_name: adguardhome
restart: unless-stopped
network_mode: host
volumes:
- /volume1/docker/adguardhome/work:/opt/adguardhome/work
- /volume1/docker/adguardhome/conf:/opt/adguardhome/conf
No ports: section needed or wanted. The container shares the host's network stack directly,
real client IPs included. The tradeoff: AdGuard now has first claim on every port on that host,
not just 53/3000/80. If anything else on the same NAS wants one of those ports, you have a
conflict, and it's the same class of conflict as the port-53 issue above, just generalized.
Bridge with explicit port mapping (what the first compose file above does) keeps AdGuard contained to its own ports, but every client shows up as the bridge gateway IP instead of its real address. Bridge mode has no clean way around that. If per-client IPs matter to you, that's what host networking is for; bridge just isn't the mode for it.
If you don't care about per-client stats and just want ad blocking for the whole network, bridge mode is simpler to reason about. If per-client visibility matters to you, host mode is the straightforward path.
DNS rewrites: giving internal services clean hostnames
A DNS rewrite is a hostname → IP override that AdGuard answers with instead of asking an upstream resolver. The pattern that makes this useful in a homelab: point an internal hostname straight at whatever's terminating TLS for your internal apps (a reverse proxy, typically), so you get a clean name and a valid certificate without needing a public DNS record for something that never leaves your network.
Example, entirely placeholder values:
app.internal.example -> 192.168.1.10
Where 192.168.1.10 is your reverse proxy's LAN IP. The proxy then routes app.internal.example
to whatever container actually serves it, and issues (or already holds) a certificate for that
name. Clients on your network get https://app.internal.example with no cert warning and no
public exposure.
To add one: in the AdGuard Home web UI, under DNS rewrites, add a domain and the IP (or hostname) you want it to resolve to. Each rewrite is independent: there's no dependency between them, and every rewrite applies immediately to every client that uses this AdGuard instance as its resolver. Nothing to configure per-client.
One shortcut worth knowing: AdGuard supports wildcard rewrites. An entry like
*.internal.example -> 192.168.1.10 matches every subdomain under internal.example in one
rule, instead of adding app.internal.example, dash.internal.example,
grafana.internal.example one at a time. If your reverse proxy already routes by hostname
(which is exactly what Caddy or Traefik do), one wildcard rewrite is usually all you need no
matter how many internal apps you add later. You only touch AdGuard again if the reverse
proxy's own IP changes.
The trap: DNS resolving is not the same as a route existing
Here's a mistake worth knowing about before you make it: a rewrite that points a hostname at a machine that's only reachable over a private overlay network (Tailscale, WireGuard, whatever) works perfectly for any client that's also on that overlay. It silently fails for any client that's LAN-only, sitting on a different subnet with no membership in that overlay.
The failure mode is confusing because DNS resolution succeeds: nslookup or dig return the
right IP immediately. The connection just times out afterward, because there's no route from
that client's subnet to the overlay IP it just got handed. It looks like a DNS problem. It's a
routing problem. If a service is only reachable via an overlay network, every client that needs
to reach it also needs to be a member of that overlay. Resolving the name correctly is not
sufficient.
Reaching the admin UI from another network
By default, AdGuard's admin API/UI binds to a specific host address, usually whatever interface it was configured against during setup. If you only ever open the admin UI from the same LAN segment the NAS is on, this is invisible. It becomes a problem the moment you try to reach it from somewhere else: a VPN client, an overlay network, a different VLAN.
The fix is setting bind_host: 0.0.0.0 in AdGuardHome.yaml, which makes AdGuard listen on
every interface instead of just the one it started on. Be clear-eyed about what that actually
does: it's not a free upgrade, it's a tradeoff. The admin UI (which can add rewrites, change
filtering rules, and see your query log) becomes reachable from every network that host has an
interface on, not just the one you meant to add. If you make this change, pair it with whatever
access control you'd normally put in front of an admin interface (a firewall rule, an auth
proxy, or at minimum a strong AdGuard admin password) rather than relying on network placement
alone to keep it private.
What I'd do differently
I'd decide on host vs bridge networking before the first rewrite goes in, not after. Moving an already-working setup from bridge to host mode later means re-checking every port that Synology service and every other container might also want. Deciding once, up front, is less work than migrating.
Related
Wake-on-LAN from Home Assistant
Home Assistant's Synology integration has no wake button. It only manages the NAS once it's on. Here's the WoL setup, and what silently breaks it.
USB Passthrough After VM Reboot
Synology Virtual Machine Manager doesn't re-attach a passed-through USB device after a VM reboot. Here's why, plus a self-healing script: ConBee II example.
Dockhand Multi-Host Dashboard
Why I run Dockhand across my Docker hosts, how the Gitea-first deploy flow works, and the API that lets an agent manage containers without SSH.
If this saved you some time, a coffee keeps the lights on and the posts coming.
☕ Buy me a coffee