Homelab Haven

Running AdGuard Home in Docker on Synology

September 12, 20268 min read

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

If this saved you some time, a coffee keeps the lights on and the posts coming.

☕ Buy me a coffee

Comments

Comments are checked before they appear.