In the first article on containers we looked inside a container to understand what it shares with the router. Now we use one for something useful: running AdGuard Home, a DNS server that blocks ads and trackers for the whole network, inside the router itself. No Raspberry Pi humming in a corner: the DNS filter lives in the router, in an isolated container. We set it up in the lab, configure it, put it to the test (it resolves, it blocks) and close it down properly.
Step 1: what AdGuard Home is, and why in a container
AdGuard Home is a DNS server that acts as a filter for the whole network: every device asks it “what is the address for this name?”, and it answers — except when the name belongs to an advertising network or a tracker, in which case it blocks it. A single control point for phones, TVs, computers, with nothing to install on each device. It also has a web UI with statistics and a log of the requests.
Why inside the router? Because the router is always on and is already the heart of the network: it is the natural place for a DNS. And with containers you don’t need an extra device. Recalling the first article: a container is not a virtual machine, it shares the router’s kernel, but brings its own userland (here the AdGuard Home image). AdGuard runs in its own space, with its own libraries, and talks to the network through a veth.
Conceptual prerequisite: Device-mode on RouterOS — containers must first be unlocked with device-mode container=yes and a physical confirmation.
Step 2: the network link
As in the first article, we give the container a veth interface on a dedicated bridge, with NAT toward the outside (AdGuard needs the Internet to query its upstream DNS):
/interface veth add name=veth-adg address=172.17.0.4/24 gateway=172.17.0.1
/interface bridge add name=ctbr
/ip address add address=172.17.0.1/24 interface=ctbr
/interface bridge port add bridge=ctbr interface=veth-adg
/ip firewall nat add chain=srcnat src-address=172.17.0.0/24 action=masquerade
The container will have the address 172.17.0.4; the router is 172.17.0.1 on the same segment.
Step 3: where AdGuard keeps its data
A container, if you delete it and recreate it, starts from scratch. But AdGuard must remember its configuration and statistics. So we give it two persistent folders from the router, mounted inside the container where AdGuard expects its files:
/container mounts add list=adg src=adg/conf dst=/opt/adguardhome/conf
/container mounts add list=adg src=adg/work dst=/opt/adguardhome/work
Two things to note. src is a folder on the router (a relative path, the router creates it on its own); dst is where it appears inside the container. And we put them in the same list (list=adg), so the container references them all with a single name.
Here the theme of the first article returns: every mount is an access you grant. But there is an important difference. mikroscope mounted the router’s /proc and /sys to watch its state; here we mount only AdGuard’s own data folders (/opt/adguardhome/...): we give the container no window onto the router. This is the healthy way to use mounts — only what the service needs, nothing of the system.
Step 4: create and start the container
We tell it where to pull images from and create the container, attaching the mount list. Note: no privileged. AdGuard is an ordinary network service, it does not need root powers over the router — so we do not give them.
/container config set registry-url=https://registry-1.docker.io tmpdir=tmp
/container add remote-image=adguard/adguardhome:latest interface=veth-adg \
root-dir=ct/adguard mountlists=adg logging=yes name=adguard
/container start adguard
After the download, the container runs. In the lab its footprint was small:
/container print
# NAME ROOT-DIR INTERFACE MEMORY-CURRENT CONTAINER-SIZE
0 R adguard /ct/adguard veth-adg 56.1MiB 43.5MiB
About 56 MiB of RAM and 43 MiB on disk for a DNS that serves the whole house. Always check the router’s free RAM and space first: on a small device it is a sum worth doing.
Step 5: the initial setup
On first start AdGuard opens a setup wizard on port 3000. From a browser, on a LAN PC, go to http://172.17.0.4:3000 (if the container is not directly reachable from your LAN, see Step 6 to expose it). The wizard asks:
- which port to put the web UI on (typically
80); - which port to put the DNS server on (
53, the standard one); - an administrator username and password.
⚠️ Warning: choose a strong password. This web UI commands the DNS of the whole network; with the credentials in hand, one decides what everyone resolves.
Once the wizard is done, AdGuard restarts on the chosen ports: 3000 closes, the web UI moves to 80 (now protected: without a login it returns 401) and the DNS on 53 is active. From here on you manage it from http://172.17.0.4/.
Step 6: using it — and watching it block
For AdGuard to actually filter, the devices must use it as their DNS. Two ways:
- The whole network: in the router’s DHCP server, announce the container’s address as the DNS. That way every device that gets its address from the router uses AdGuard with no manual setup.
- The router itself:
/ip dns set servers=172.17.0.4to route the router’s own requests through AdGuard too.
Let’s test in the lab. With DNS pointed at the container, a normal name resolves:
/ip dns set servers=172.17.0.4
:put [:resolve "example.com"]
104.20.23.154
Now we add a blocking rule (from the web UI, or via the API) for an advertising domain and try again:
:put [:resolve "doubleclick.net"]
0.0.0.0
AdGuard answered 0.0.0.0: the name is blocked, the browser will not reach that server. In the web UI, under statistics, the two requests show up at once — one allowed, one blocked:
num_dns_queries: 2
num_blocked_filtering: 1
And the block lists, who adds them? You don’t compile them by hand. AdGuard Home comes with a catalog of ready-made lists: the AdGuard DNS filter is already active as soon as setup is done (in the lab, over 185,000 rules), and other well-known lists (AdAway, OISD, the hagezi ones…) are there to switch on with one click under Filters → DNS blocklists. They are online lists: AdGuard downloads them from a URL and updates them on its own at regular intervals (by default once a day), so they stay current with no effort from you. You can add more by pasting their URL, write your own rules by hand, and see in the log who asked for what. But the basic mechanism is the one you have just seen.
In one click: the “app” version (RouterOS’s Apps menu)
Everything we did by hand — image, veth, bridge, NAT, mounts, ports — recent RouterOS versions let you ask for in a single command. It is called Apps (/app): a layer that sits on top of containers and orchestrates them from a catalog. An “app” is a recipe in Docker-Compose style (a YAML file) describing image, ports and volumes; RouterOS reads it and builds the container, the network and even an HTTPS address for the web UI, all on its own.
The teaching point is exactly this: the app does, under the hood, the very Steps 2-4 you just saw by hand. Having done them by hand once is what lets you understand what really happens when you press “one click”.
Three things are needed, once:
- device-mode with
containerandpartitionsunlocked (plus the physical confirmation) — see the device-mode KB. - a fast external disk (USB or NVMe) formatted
ext4: unlike the manual container,/appwants real storage (the docs mention ≥100 MB/s), not the internal flash. - a setup step where you tell
/appwhich disk, which LAN bridge and which router IP to use.
# unlock container + partitions (requires the physical confirmation on the device)
/system/device-mode/update container=yes partitions=yes
# after formatting the external disk as ext4 (Disks menu), point disk/bridge/ip
/app settings set disk=<external-disk> lan-bridge=<lan-bridge> router-ip=<router-ip>
# install AdGuard from the catalog, in one command
/app enable <app-name>
From here RouterOS pulls the image (in the Dojo recipe we keep it pinned to a precise version, not latest), creates and starts the container, and generates a web-UI address like https://<app-name>.<serial>.routingthecloud.com.
Security note (verified). That cloud name is public, but it resolves to the router’s private LAN IP: from the Internet it points to a non-routable address. In practice AdGuard’s web UI stays reachable only from your own network and is not exposed to the Internet by the MikroTik cloud. The manual method’s rule still holds: do not forward that port toward the WAN.
Where it works, and where it doesn’t — honestly. The one-click deploy completes on real hardware (arm64/x86 devices with a fast external disk): image pulled, container created and started, UI responding. On a CHR / in a virtual machine with a file-backed disk, instead, /app enable stalls at “wait for reverse proxy” and never creates the container: /app needs real fast storage, which a file-backed virtual disk does not guarantee. On a CHR with a real, fast, dedicated disk it may well work, but we have not verified that yet. The /app menu is a recent feature, still maturing: we trust MikroTik will make it solid across more setups. And if meanwhile your deploy hangs at “wait for reverse proxy”, the manual method above works everywhere, today, on every stable version — and without an external disk.
Step 7: closing it down properly
- DNS toward the LAN only, never toward the WAN. A DNS server open to the Internet becomes an open resolver: it gets abused for amplification attacks against others. With the firewall, the container’s port 53 must never be reachable from outside. Expose to the LAN only what is needed.
- The web UI protected and not public. A strong password (Step 5) and no forwarding of the web UI port toward the Internet.
- No
privileged. AdGuard does not need it: left without, any flaw in the service stays inside the container and does not touch the router. - Mounts to a minimum. Only AdGuard’s two data folders. No reason to mount it anything else.
- Keep the image updated. A DNS that sits in the middle of all the traffic must be kept up to date; pull the image again when a new version comes out.
- Cap the resources with
memory-max, so a container problem does not eat the router’s RAM.
Frequently asked questions
Is device-mode needed for AdGuard too?
Yes: AdGuard runs in a container, and containers must be unlocked once with device-mode container=yes plus the physical confirmation. Once done, it applies to all containers.
Can I use it without exposing the container to the LAN?
If you want the devices to query it directly, their DNS must be able to reach it: either the container sits on a LAN bridge, or the router forwards port 53 to the container from the inside only. In both cases port 53 must never face the WAN.
If I delete the container, do I lose the configuration?
No, if you did the mounts in Step 3: configuration and statistics live in the router’s folders (src), not inside the container. Recreating the container with the same mounts, AdGuard finds everything again.
How much does it really weigh on the router?
In the lab about 56 MiB of RAM and 43 MiB on disk, with CPU near zero at idle. It depends on how many block lists you load and how many requests it serves: more lists and more clients, more RAM. On a small device, check first.
Can AdGuard see inside the router, like mikroscope?
No, and it is deliberate. Here there is no privileged and we don’t mount /proc or /sys: AdGuard sees only its own filesystem and its two data folders. It is a guest service, not an observer of the router.
Tested in the lab on CHR RouterOS 7.25 (stable): the container package and device-mode container=yes; an adguard/adguardhome container on a veth with a dedicated bridge and NAT, with two persistent mounts (list=adg, /opt/adguardhome/conf and /opt/adguardhome/work) and no privileged; the initial setup wizard on port 3000, then the web UI on port 80 (protected, 401 without credentials) and DNS on port 53; resolving a name through the container (example.com) and blocking an advertising domain (doubleclick.net → 0.0.0.0) confirmed in the statistics (2 requests, 1 blocked); measured footprint 56.1 MiB of RAM and 43.5 MiB on disk.



