Manual

Recipe: AdGuard Home or Pi-hole in a container

A DNS filter next to RouterOS on the same device, with an honest answer about when that is a good idea.

Goal: AdGuard Home or Pi-hole runs on the router itself, your clients use it as their DNS server, and you do not have to add a device. Read the last section first; it says when not to do this.

What you need

  • A device with ARM64 or x86. The Containers section only appears when your model's architecture allows it; on anything else the tool tells you containers require ARM64 or x86, and that is the end of it.
  • External storage: a USB stick, SSD or NVMe. The internal flash of a RouterBOARD is small and wears out from writing.
  • The container package, which you download from MikroTik and install on the router yourself.
  • Physical access to the device, within five minutes of pasting. This is the only part of the configurator where you have to be standing next to it.

Step 1: check whether your device can really do this

The architecture is the only check the tool makes, and it is not enough. What decides it in practice is memory:

DeviceArchitectureRAMContainer?
hAP ax2 / hAP ax3ARM 64bit1 GByes
RB5009ARM 64bit1 GByes
cAP axARM 64bit1 GByes
CCR2004ARM 64bit4 GByes, with room
RB4011, CRS326, L009ARM 32bit512 MB to 1 GBno
hAP ac2ARM 32bit128 MBno
hEX, hEX SMMIPS256 MBno

That gigabyte is shared with RouterOS itself, with your connection table and with everything else running. AdGuard Home uses roughly 100 to 150 MB at rest, and more as you load more lists and log more queries. On 1 GB that fits next to an ordinary configuration; below half a gigabyte you do not start.

Storage works out the same way. The image, its layers, the temporary files during the pull and the query log all have to live somewhere: count on a few hundred megabytes plus a log that grows, on a stick or an SSD and not on the internal flash.

Step 2: the settings in the section

  1. Storage: the disk name, disk1 by default. Use /disk print to see what yours is called; on a USB stick it is often usb1-part1. With the wrong name here, everything writes to a path that does not exist.
  2. Container network: the router's address in that network, 172.17.0.1/24 by default. Pick something that occurs nowhere else in your network.
  3. Registry URL: https://registry-1.docker.io, unless you run your own mirror.

Below that, the container itself. For AdGuard Home:

  • Name: adguard. That name comes back as the comment, as the name of the veth interface and as a directory on your disk.
  • Image: adguard/adguardhome:latest. Better to pin a version than to use latest, so a restart tomorrow does what it did today.
  • IP in the container network: 172.17.0.2, in the same subnet as the address above.
  • Environment variables: TZ=Europe/Amsterdam for example, one per line.
  • Mounts: work:/opt/adguardhome/work and conf:/opt/adguardhome/conf, each on its own line. The tool turns those into directories on your disk, so your settings survive a restart.
  • Forward ports from the LAN: tcp:3000 for the first-run setup, then tcp:80 for the web interface.

The default row in the tool is Pi-hole, with WEBPASSWORD=changeme as an example. Environment variables end up in the script literally. Put a password in there and that password is in the script and in whatever you share.

Step 3: what the script does

  • As its first line, /system device-mode update container=yes. That asks for a physical confirmation within five minutes: press the reset button, or power-cycle. Skip it and nothing happens. In practice you therefore paste in two rounds: paste the script, confirm, then run the container part again. The script says so in a comment.
  • A bridge called containers, the router address on it, and a masquerade rule so the container can reach the outside.
  • /container config with your registry and the directories for temporary files and layers on your disk.
  • An /interface veth called veth-adguard, with your address and the router as gateway, added as a port to that bridge. The veth is the cable between RouterOS and the container: the container sees it as its network card, the router sees it as an interface in the bridge.
  • A dst-nat rule per forwarded port, with in-interface-list=LAN and dst-address-type=local. That means you reach the container on the router's own address, on that port. AdGuard on tcp:80 is therefore on your router's LAN address, not on 172.17.0.2.

The script does not start your container. The image has to be pulled first, which takes time and can fail. The last line of the section shows how to do it yourself:

/container start [find comment="adguard"]
/log print where topics~"container"

Step 4: pointing DHCP at it

There are two ways, and each has a price.

Let the router forward (recommended). In DNS, set Upstream DNS to Custom and enter 172.17.0.2. Leave Router as DNS server for the LAN on. Your clients keep using the router, and the router asks the container. You do not have to forward port 53 at all, and it works in every VLAN, an isolated one included. The price: AdGuard only ever sees the router as its client, so per-device statistics and per-device rules are gone.

Send clients straight to the container. In LAN & DHCP, or per VLAN in the VLAN table, set DNS server for DHCP clients to Custom and enter that address. Now AdGuard sees every device separately. The price: clients in an isolated VLAN cannot reach the container network, so you have to open that direction in the matrix, and if the container stops, nobody has DNS any more.

In both cases Force LAN clients through the router in the DNS section helps against devices with a hard-coded 8.8.8.8. It does not help against DNS over HTTPS in a browser.

Testing

  1. /system/device-mode/print: does it say container: yes?
  2. /container print: is it running, and what is its status?
  3. /log print where topics~"container" when in doubt. This is also where you see why an image will not download.
  4. Open the web interface on your router's LAN address, on the port you forwarded.
  5. From a client, look up a domain that should be blocked, and check that it shows up in AdGuard's query log.
  6. Reboot the router and see whether the container comes back by itself. It should, because start-on-boot=yes is set.
  7. /system resource print while it is busy: how much memory is left?

What the tool does not have here

  • No field for a memory or CPU limit per container. A container that eats memory does so at the expense of the routing.
  • No health check and no restart policy. If the container falls over, it stays down until the next start.
  • No automatic image updates. A new version is something you pull yourself.
  • No second DNS server as a fallback. If you add one next to the container, clients will use that one too, and your filtering has become a suggestion.

The honest advice

A container on the router is attractive for one small thing on a device with room to spare. For DNS that is usually not the case, and here is why: DNS is the service everything waits for. When your router reboots for an update, your DNS goes with it. When the container files go bad on a USB stick that has been in the meter cupboard for three years, the whole house stops. And a RouterOS upgrade can ask for device-mode to be confirmed again while you are nowhere near the device.

A separate box, a Raspberry Pi or a small mini PC, costs you a power socket and solves all of that: the router stays a router, updates and reboots are independent, and you can put a second one next to it. Do it on the router if the device is oversized anyway and the service is not critical, not to save yourself the box.

Further reading: Containers on RouterOS, DNS and Choosing your device.

Want to try it right away? Open the configurator