Adventures in Networking

Main menu:

Archive for March, 2026

Run BIND9 DNS in a Container on MikroTik RouterOS

This guide shows how to run a BIND9 DNS server in a container on MikroTik RouterOS, using external storage and RouterOS container support. The setup was tested on the MikroTik RB5009UG+S+IN, but should work on most MikroTik routers that support containers.

The container runs BIND independently from RouterOS DNS and uses mounted storage for configuration, zones, and cache data.

Why Run BIND Instead of RouterOS DNS?

For many years I ran BIND on dedicated hardware—first repurposed thin clients and later Raspberry Pis.

A couple of years ago, I simplified my home network by removing those servers and relying on RouterOS DNS forwarding with static host entries. That worked well, but eventually I began expanding my home lab again and wanted the full flexibility of a real DNS server:

  • authoritative zones
  • advanced caching configuration
  • DNSSEC control
  • full configuration flexibility

Running BIND in a container on the router turned out to be a simple and efficient solution.

Requirements

To run a BIND container on RouterOS you need:

  • A MikroTik device that supports RouterOS containers
  • External USB storage
  • A compatible container image
  • A dedicated container subnet

The RB5009 hardware is more than capable of handling a small DNS container.

Storage for RouterOS Containers

RouterOS containers require external storage.

I initially tested using a USB thumb drive, but those are not designed for continuous disk activity. Instead I connected an NVMe SSD through a USB enclosure and formatted it on the router.

After connecting the disk:

  1. Navigate to System → Disks
  2. Select the device (for example usb1)
  3. Format it using ext4

RouterOS will then expose the disk through the Files interface.

Installing Container Support on RouterOS

Download the container package from:

https://mikrotik.com/download

Make sure the package matches:

  • RouterOS version
  • CPU architecture

Upload the package through WinBox → Files, then reboot the router.

Next enable container support:

/system/device-mode/update container=yes

Perform a cold reboot (power cycle) after enabling containers.

Preparing the BIND Directory Structure

Create the following directories on the USB disk:

usb1/bind9
usb1/bind9/cache
usb1/bind9/etc
usb1/bind9/hints
usb1/bind9/lib
usb1/bind9/log

These correspond to the directories used inside the container:

Container DirectoryPurpose
/etc/bindBIND configuration
/var/cache/bindDNS cache
/var/lib/bindzone storage
/var/loglogging
/usr/share/dnsDNS hints

If you already run BIND elsewhere, you can copy your existing configuration into the /etc/bind directory.

Selecting a BIND Container Image

RouterOS can download container images directly from Docker Hub.

After some experimentation I found a container built specifically for MikroTik:

https://hub.docker.com/r/tecspace/bind9-mikrotik

This avoids architecture compatibility issues that sometimes occur with official images.

Creating Container Mount Points

Container mounts connect RouterOS directories to the container filesystem.

Create mounts mapping the RouterOS directories to the container paths:

ListSrcDst
MOUNT_BIND9/usb1/bind9/cache/var/cache/bind
MOUNT_BIND9/usb1/bind9/etc/etc/bind
MOUNT_BIND9/usb1/bind9/hints/usr/share/dns
MOUNT_BIND9/usb1/bind9/lib/var/lib/bind
MOUNT_BIND9/usb1/bind9/log/var/log

Environment Variables

Environment variables can override container defaults.

Example configuration:

ListKeyValue
ENV_BIND9BIND9_STAY_ON1
ENV_BIND9BIND9_CMD_OPTS-g -4 -u named
ENV_BIND9BIND9_CFG_OPTSversion “hidden”;directory “/var/cache/bind”;listen-on {any;};listen-on-v6 {none;};recursion yes;allow-query {0.0.0.0/0;};d/system/device-mode/update container=yesnssec-validation yes;max-cache-size 64M;

Most configuration should still live in standard BIND configuration files.

Container Networking Configuration

Create a dedicated bridge for containers:

/interface/bridge/add name=bridge_containers
/ip/address/add address=172.16.0.1/24 interface=bridge_containers

Create a virtual Ethernet interface:

/interface/veth/add name=veth1 address=172.16.0.2/24 gateway=172.16.0.1
/interface/bridge/port/add bridge=bridge_containers interface=veth1

Enable NAT so the container can reach the internet:

/ip/firewall/nat/add chain=srcnat action=masquerade src-address=172.16.0.0/24

Creating the BIND Container

Configure the container registry:

/container/config/set registry-url=https://registry-1.docker.io tmpdir=usb1/tmp

Create the container:

/container/add \
remote-image=tecspace/bind9-mikrotik:1.0 \
interface=veth1 \
root-dir=usb1/images/bind9 \
mountlists=MOUNT_BIND9 \
envlist=ENV_BIND9 \
start-on-boot=yes \
name=bind9

Monitor the container download:

/container/print

Once the status shows stopped, start the container:

/container/start bind9

Using RouterOS to Forward DNS to the BIND Container

Once the server is operational, you may want the router itself to use the container.

Example RouterOS configuration:

/ip dns
set servers=172.16.0.2 allow-remote-requests=yes

Performance Observations

The BIND container uses minimal resources and has no noticeable impact on router performance.

In testing, DNS responses from BIND were actually slightly faster than RouterOS DNS forwarding—only by a few milliseconds, but consistently measurable.

Running BIND directly on the router turned out to be an elegant solution.

Testing DNS Performance

For DNS benchmarking I recommend:

https://www.grc.com/dns/benchmark.htm

GRC DNS Benchmark remains one of the best tools for comparing resolver performance.

BIND DNS Container on Mikrotik RouterOS

For a long time, I’ve run my own DNS (typically just BIND), originally on re-purposed thing clients, then on Raspberry Pis. A year or two ago, being very busy and working long hours, I decided I didn’t have much time to maintain my home network and started to simplify it. After reading about RouterOS’s capabilities for DNS forwarding with static host entries, I gave it a try. It performed well, and while I would lose capabilities, it did everything I needed it to at the time. I copied all the static entries from my zone file to my primary router, then archived my DNS server configs and shut them down. It’s worked well for a while, but I am now beefing up my home network/lab and decided I wanted full DNS capabilities back.

Since I last ran BIND on a Raspberry Pi, I have upgraded my home gateway to a Mikrotik RB5009UG+S+IN. This is a very capable little machine, with a GB of RAM, a quad-CPU ARM processor, and a GB of flash. It is also capable of running containers natively! Docker and the whole container concept emerged after I shifted roles in my career, going from smaller organizations where I had broader technical responsibilities, to larger organizations where I was much more focused on narrower network/security domains (and more time managing organizational politics). Consequently, I have never taken the time to get up to speed on something that’s common to most newer IT pros. This was a perfect opportunity to fix that!

After researching the basics, here is what I learned in my crash course on containers in general, and setting one up on a Mikrotik device.

Basic requirements:

  • A Mikrotik device with sufficient CPU and RAM
  • An external USB-connected storage device
  • A container image with BIND9 that supports your hardware
  • Choose a subnet for the container

Mikrotik hardware

This was not as easy to calculate scientifically. I know that BIND doesn’t require much in terms of CPU or RAM, but how do you figure out requirements for a container? I could never load my Raspberries enough to see any issues, so after looking at average CPU and memory utilization on the RB5009, I made an educated guess that it would be a non-issue.

Storage device

I originally tested a low-profile SanDisk thumb drive, and while I’m sure that would have worked for a test, they are not designed to be used continuously as a disk drive and knew that wasn’t a good idea if I intended to use it “for real.” So, I sacrificed my NVMe to USB-C SSD enclosure for the cause, popped in a 250GB stick I had lying around, and plugged it in to the router. (Note: I have no idea exactly how RouterOS manages “disks,” but having plugged in the thumb drive first, the NVMe seems to be permanently labeled as “usb2″—operationally immaterial but drives my brain crazy!)

BIND9 Container

Being completely inexperienced with the whole Docker/container world, I spent a couple hours reading up on it and trying to figure out how to find and download a container. (Yes, yes, eventually I’ll figure out how to build my own, but I’m a newbie right now.) After a few missteps, including thinking I needed to download an image to my workstation first, and trying the official ISC container image without success (CPU architecture mismatch, despite what it said on the Docker Hub description), I came across a pre-built BIND9 container for Mikrotik, see https://hub.docker.com/r/tecspace/bind9-mikrotik. In this process I learned about the Docker Hub Container Image Library, and discovered that RouterOS can download an image directly, given the name from the Hub.

Process

I mostly used WinBox, though I did sometimes resort to the terminal—remember, I’m new at this, and as much experience as I have with Mikrotik, I don’t RouterOS daily, so yes, I used my crutch. (I did also discover that you need WinBox 4.x, as 3.x has bugs. However, that actually led me to a pleasant surprise—WinBox 4.x has a native Linux binary, no more WINE needed. Not that WINE was a problem, but this is a nicer experience. The little things that make me happy.)

In any case, here’s what I went through to get this up and running (in WinBox, and YMMV):

  • Install container package
    • Download the Mikrotik container package from https://mikrotik.com/download (match your CPU architecture and RouterOS version)
    • Upload it to Files by dragging it onto WinBox
    • Reboot the router
  • Enable containers and reboot
    • Enter this at the terminal: /system/device-mode/update container=yes
    • Cold boot (power cycle) the router
  • Format Storage Disk
    • Plug storage disk into USB
    • Go to System > Disks
    • If you don’t see usb1 (or maybe usb2, like for me) in the list, the system didn’t recognize it (I had this happen with a cheap NVMe to USB enclosure I got off Amazon—junked that and used the Best Buy enclosure and had no further issues)
    • Click Format Drive, select ext4 and let ‘er rip
  • Create file system structure
    • Go to Files
      • Once you’ve attached and formatted the disk, you will see a folder representing it here (usb1)
      • Files and directories appear to be stored as a Key > Value pair, with the Key representing the full path with slashes.
    • Click New > Directory and create the following
      • File Name = bind9, Directory = usb1 (base directory to hold everything related to BIND)
      • File Name = cache, Directory = usb1/bind9 (/var/cache/bind)
      • File Name = etc, Directory = usb1/bind9 (/etc/bind)
      • File Name = hints, Directory = usb1/bind9 (/usr/share/dns – discovered I needed this when I copied my config from my Raspberry DNS server)
      • File Name = lib, Directory = usb1/bind9 (/var/lib/bind)
      • File Name = log, Directory = usb1/bind9 (/var/log)
    • Upload config from existing BIND setup
      • I was able to upload all of my existing config files to the appropriate locations in Files and get things to work with no changes from my original Debian 12 BIND9 setup, so long as I created all locations and uploaded all referenced files.
  • Create container mounts
    • Go to Container > Mounts > New and create the following:
      • List = MOUNT_BIND9, Src = /usb1/bind9/cache, Dst = /var/cache/bind
      • List = MOUNT_BIND9, Src = /usb1/bind9/etc, Dst = /etc/bind
      • List = MOUNT_BIND9, Src = /usb1/bind9/hints, Dst = /usr/share/dns
      • List = MOUNT_BIND9, Src = /usb1/bind9/lib, Dst = /var/lib/bind
      • List = MOUNT_BIND9, Src = /usb1/bind9/log, Dst = /var/log
  • Create environment variables
    • Note: I created these all as “disabled,” as I was able to accept the defaults and let BIND pull everything else from the config files. See notes on Docker Hub for the container, as well as BIND documentation.
    • Go to Container > Envs > New and create the following:
      • List = ENV_BIND9, Key = BIND9_STAY_ON, Value = 1
      • List = ENV_BIND9, Key = BIND9_CMD_OPTS, Value = -g -4 -u named
      • List = ENV_BIND9, Key = BIND9_CFG_OPTS, Value = version “hidden”;directory “/var/cache/bind”;listen-on {any;};listen-on-v6 {none;};recursion yes;allow-query {0.0.0.0/0;};d/system/device-mode/update container=yesnssec-validation yes;max-cache-size 64M;
  • I relied heavily on a Mikrotik tutorial for all steps going forward, but posting them here in case the link goes stale
  • Network setup
    • Create new bridge for container networking:
      • /interface/bridge/add name=bridge_containers
    • Assign an IP address to the bridge:
      • /ip/address/add address=172.16.0.1/24 interface=bridge_containers
    • Create a new virtual interface:
      • /interface/veth/add name=veth1 address=172.16.0.2/24 gateway=172.16.0.1
    • Assign interface to bridge:
      • /interface/bridge/port add bridge=bridge_containers interface=veth1
    • Create a NAT for outgoing traffic:
      • /ip/firewall/nat/add chain=srcnat action=masquerade src-address=172.16.0.0/24
  • Configure registry info (I am not positive I had to do this)
    • /container/config/set registry-url=https://registry-1.docker.io tmpdir=usb1/tmp
  • Create the container
    • /container/add remote-image=tecspace/bind9-mikrotik:1.0 interface=veth1 root-dir=usb1/images/bind9 mountlists=MOUNT_BIND9 envlist=ENV_BIND9 start-on-boot=yes name=bind9
  • Check the status of the container and wait until downloading/extracting has been finished and the status=stopped:
    • /container/print
  • Start the Containter:
    • /container/start bind9

All done

If you’ve done everything right, you should now have a functioning DNS server, running in a container on your router. From what I’ve observed, it uses very little CPU and RAM, causing essentially no impact to the router. Forwarded queries are resolved a hair faster (milliseconds) than the router’s own DNS forwarding. I consider this a great success!

A note about testing DNS performance

Quick shout out to my favorite DNS benchmarking tool: https://www.grc.com/dns/benchmark.htm