Six gotchas deep in RouterOS containers

RouterOS containers use the same vocabulary as Docker. Bridge, veth, mount, registry pull, start-on-boot. You set up the network the same way. The config goes where you expect.

Then you need a cold power cycle to enable the feature. Not a reboot. A real, physical, pull-the-power-cable power cycle.

This is a catalog of every gotcha hit deploying blocky in a RouterOS container on a hAP ax3 running RouterOS 7.23.2. Each one cost between ten minutes and an hour. Combined, they turned a "deploy a container" afternoon into a proper project.

1. Device-mode requires physical confirmation

/system/device-mode/update container=yes

RouterOS responds: "please activate by turning power off or pressing reset or mode button in 5m00s."

A soft /system/reboot does not count. The timer expires. Nothing changes. You need to walk to the router and pull the power cable.

This is deliberate. Once container mode is on, containers can be added and started remotely — over SSH, from anywhere. MikroTik requires physical access for the initial enablement so a remote attacker can't silently spin up a backdoor container.

Two sharp edges:

  • Killing the SSH session that issued the update cancels the pending confirmation. Keep that terminal open while you walk to the router.
  • Three unconfirmed attempts trigger a lockout. Miss the 5-minute window three times and you're power-cycling to reset the counter.

2. The container package silently reverts

The container package shows up in /system/package/print with an XA flag — disabled, available-but-not-downloaded. The intuitive approach:

/system/package/enable container
/system/reboot

The router reboots. The package reverts to disabled. No error.

The reliable path: download container-7.23.2-arm64.npk from MikroTik's all_packages archive, upload it to the router, reboot. The package installs from the local file and stays.

The package is version-pinned to the RouterOS release. Upgrade RouterOS and the container package upgrades with it, but version mismatches from manual uploads can leave containers stopped.

3. list=, not name=

Container mounts take a list= parameter:

/container/mounts/add list=blocky-cfg src=usb1-part1/blocky-cfg dst=/blocky

The container references it with mountlists=:

/container/add ... mountlists=blocky-cfg ...

Not name=. Not mount=. Not volumes=. list= and mountlists=.

The naming is internally consistent — RouterOS uses "list" as a grouping concept across features (address-list, interface-list). But if your muscle memory says Docker, you'll type name= first, get an unhelpful error, and look up the docs. This is the smallest gotcha here and it still cost ten minutes.

4. Logging action names: no hyphens

/system/logging/action add name=blocky-log target=disk \
  disk-file-name=usb1-part1/blockylog

Fails. On this hAP ax3 (RouterOS 7.23.2), logging action names accept letters and digits only. No hyphens, no underscores — confirmed by trial.

/system/logging/action add name=blockylog target=disk \
  disk-file-name=usb1-part1/blockylog disk-lines-per-file=5000 disk-file-count=10

Works.

RouterOS also won't auto-create log directories. Point a disk logging action at a nonexistent path and it silently fails to write. No error, no warning, no logs. Found this one by noticing the log file never appeared.

5. Changing memory-high restarts the container

Setting a memory soft-cap on a running container:

/container/set 0 memory-high=256M

The docs describe memory-high as a soft cgroup throttle. On this hAP ax3, applying it to a running container restarted the container entirely — observed on RouterOS 7.23.2, whether or not the limit changes the effective value. If the container is your DNS server and the dst-NAT redirect is active, the entire LAN loses DNS for a few seconds while the container restarts and reloads its blocklists.

The safe approach:

/ip firewall nat disable [find comment="blocky-dns-redirect"]
# router's own DNS serves the LAN during the gap
/container/set 0 memory-high=256M
# wait for container to restart and load lists
/ip firewall nat enable [find comment="blocky-dns-redirect"]

Disable the redirect first so the router's built-in DNS covers the gap. Change the setting. Wait for the container to come back. Re-enable.

6. The healthcheck probes the wrong port

/container/print marks the container with a U in its flags column — the healthcheck is failing.

blocky's Docker image includes a HEALTHCHECK that probes port 53. We serve on 6969 because the image runs as USER 100 and can't bind below 1024. The healthcheck probes a port that nothing listens on.

The fix requires stitching three separate pieces of documentation together — and then getting the syntax right:

  1. RouterOS 7.23+ has a healthcheck-cmd property that overrides the image's baked-in healthcheck.
  2. blocky ships a healthcheck subcommand at /app/blocky healthcheck.
  3. That subcommand accepts -p <port> (default: 53).

The obvious attempt:

/container/set [find name=blocky] healthcheck-cmd="/app/blocky healthcheck -p 6969"

This silently fails. The container stays U. The image is distroless — no /bin/sh — so RouterOS's shell-form healthcheck has nothing to exec the string with. You need the exec/array form, which bypasses the shell entirely:

# disable DNS redirect first — setting healthcheck-cmd restarts the container
/ip firewall nat disable [find comment="blocky-dns-redirect"]
/container/set [find name=blocky] healthcheck-cmd="CMD,/app/blocky,healthcheck,-p,6969"
/ip firewall nat enable [find comment="blocky-dns-redirect"]

The CMD,binary,arg,arg syntax passes the argv directly. The flag flips to H. Healthy.

Four facts, zero of them documented in the same place: healthcheck-cmd is in the RouterOS manual, the healthcheck subcommand is in blocky's Dockerfile, the -p flag is in blocky's source code, and the CMD, exec form is in MikroTik's container docs under an unrelated example. Without that trail, the natural response is stop-on-unhealthy=no — which works, but leaves a fixable problem unfixed because the "fix" itself has a trap.

Sources