# Requirements

What the router needs before an install — RouterOS 7.24 or later, a container-capable architecture, the container package and device-mode — what your computer needs, and how doctor checks each.

Source: https://docs.jmrp.io/mikroscope/install/prerequisites/

Three things on the router must be in place before `install` can write anything, and the tool
cannot do them for you. `mikroscope doctor` checks every requirement read-only, in one ssh connect,
and prints the command or the physical step that fixes each one that is missing.

| Requirement         | Value                                                 | `doctor` prints                           |
| ------------------- | ----------------------------------------------------- | ----------------------------------------- |
| RouterOS            | 7.24 or later                                         | `RouterOS 7.24 or later`                  |
| Architecture        | `arm64`, `arm` or `x86_64`                            | `architecture has a container package`    |
| `container` package | installed and enabled                                 | `container package installed and enabled` |
| Device mode         | `container=yes`                                       | `device-mode container=yes`               |
| Free memory         | at least `--memory-max`, 64 MiB by default            | `free memory ≥ <size>`                    |
| Free storage        | the extracted root, plus the tar on the tar route     | `free flash ≥ <size> (…)`                 |
| Interface list      | the one `--iface-list` names, `LAN` by default        | `interface list <list> exists`            |
| ssh                 | an admin user, with a key or an ssh agent             | none; the connect itself                  |

> **One step needs your hands on the router**
>
> [`device-mode container=yes`](https://help.mikrotik.com/docs/spaces/ROS/pages/93749258/Device-mode)
> cannot be set remotely. After the command, RouterOS waits five minutes for someone to press the
> reset or mode button, or to cut the power. No ssh session, API call or option of this tool can do
> that step. Plan to be at the router.

## Router

The router must run RouterOS 7.24 or later on arm64, arm (32-bit RouterOS) or x86_64. MIPS, TILE
and PPC routers have no `container` package.

7.24 is the floor because the container step writes `privileged=`, an attribute RouterOS
[added in that release](https://download.mikrotik.com/routeros/7.24/CHANGELOG) ("container - added
ability to run containers in privileged mode"). `--privileged=false` changes the value written, not
whether it is written. `doctor` marks an earlier release `MISSING`, and the install script stops
there with nothing written. [Privileged mode](https://docs.jmrp.io/mikroscope/limits/privileged/) has what the
attribute changes.

`install` reads the architecture from the router (`--arch auto`, the default), and each route
takes the agent build that matches:

| Your device                                    | `architecture-name` | `--arch` | Agent build            |
| ---------------------------------------------- | ------------------- | -------- | ---------------------- |
| RB5009, CCR2004, hAP ax³ and other 64-bit ARM   | `arm64`             | `arm64`  | `linux/arm64`          |
| hEX Refresh / hEX S (2025), any EN7562CT board  | `arm`               | `arm`    | `linux/arm/v5`         |
| Other 32-bit ARM (hAP ac², hAP ax², …)          | `arm`               | `arm`    | `linux/arm/v5` or `v7` |
| CHR and x86 RouterOS                            | `x86_64`            | `amd64`  | `linux/amd64`          |

**32-bit ARM is two things, not one.** MikroTik's
[container documentation](https://help.mikrotik.com/docs/spaces/ROS/pages/84901929/Container)
states that devices with the EN7562CT CPU support only arm32v5 container images, while its other
32-bit ARM boards run an ARMv7 userland. An ARMv5 binary runs on both;
an ARMv7 one does not run on the first, and it fails as an `exec format error` in the container log
after a successful install. So `--goarm` defaults to **5**, the level that starts everywhere, and
the image declares the matching variant. `--goarm 7` builds the ARMv7 one for a board where that
instruction set is wanted.

With `--remote-image` the router picks the build itself: the published index carries `linux/amd64`,
`linux/arm64`, `linux/arm/v7` and `linux/arm/v5`. On `arm`, which of the two 32-bit builds RouterOS
pulls is not known, so `doctor` warns there; if the container stops with `Exec format error`,
install from the ARMv5 tar ([Offline install](https://docs.jmrp.io/mikroscope/install/offline/)).

## Container package

Download the `container` package for your architecture and RouterOS version from mikrotik.com,
upload it to the router and reboot. If it is there and disabled, run
`/system/package/enable container` and reboot. `doctor` counts the package only when it is installed
and not disabled. To read it on the router:

```routeros
/system/package/print where name="container"
```

## Device mode

1. Run, on the router's terminal:

   ```routeros
   /system/device-mode/update container=yes
   ```

2. RouterOS answers with the step it waits for. A router says:

   ```text
   update: please activate by turning power off or pressing reset or mode button in 5m00s
   ```

   CHR says `update: turn off power in 5m to activate changes`.

3. Within those five minutes, press the reset or mode button, or cut the power. On CHR, power the
   virtual machine off and on. If nobody does, the change is cancelled.

After three failed attempts the router says `too many unsuccessful attempts … to reset
attempt-count` and needs a power cycle, or a press of the reset or mode button, before it accepts
another. `/system/device-mode/print` shows `container: yes` once it is done.

## Preflight checks

`doctor` prints `device:` with the board, the RouterOS version and the architecture, then one line
per check marked `ok`, `MISSING` or `WARN`, with what it found in parentheses and, for a missing one
or a warning, a `fix:` line. It ends with `doctor: every prerequisite is met`, or fails with
`N prerequisite(s) missing; nothing was written`. A `WARN` is advice: it never changes that ending,
and `install` goes ahead past it. `install` runs the same checks first unless you pass `--no-doctor`.

The checks doctor runs:

| Check, as printed | Passes when | The fix it names |
| --- | --- | --- |
| RouterOS 7.24 or later | `/system/resource` reports a `version` of 7.24 or later, read with or without a patch number and whatever the channel, as in `7.24 (stable)` or `7.25rc1 (testing)` | upgrade RouterOS to 7.24 or later (`/system/package/update`), and the `container` package with it |
| architecture has a container package | the router's `architecture-name` is `arm`, `arm64` or `x86_64`, the architectures MikroTik publishes a `container` package for | none: no agent can run on this router |
| the router picks the image's architecture | with `--remote-image`: always, naming the router's architecture, because RouterOS picks it from the image's multi-architecture index. A warning on `arm`, where the index holds both `linux/arm/v5` and `linux/arm/v7` and which one RouterOS pulls is not known | on `arm`, if the container stops with `Exec format error`, install from `mikroscope-agent-armv5.tar` with `--agent-tar` |
| architecture matches the --agent-tar image | with `--agent-tar`: the tar's own architecture is the router's (`amd64` for `x86_64`); `--arch` is not needed | download the release asset it names, `mikroscope-agent-<arch>.tar` |
| architecture read from the router | with neither image flag and `--arch` unset (or `auto`): always; `install` and `upgrade` build or load the image for the architecture doctor read | none |
| architecture matches --arch <arch> | with an explicit `--arch` and neither image flag: the router's `architecture-name` is the one `--arch` maps to (`arm64`, `arm`, `x86_64`) | re-run with the `--arch` it names, or leave `--arch` out so that `install` reads it from the router |
| container package installed and enabled | a `container` package exists with `disabled=no` | download the `container` package for this architecture and RouterOS version, upload it and reboot; when it is there and disabled, `/system/package/enable container` and reboot |
| device-mode container=yes | `/system/device-mode` reports `container=yes` | `/system/device-mode/update container=yes`, then confirm it as the console asks: on a router that says `update: please activate by turning power off or pressing reset or mode button`, press the reset or mode button or cut the power; on CHR, which says `update: turn off power in 5m to activate changes`, power the VM off and on again within 5 minutes |
| free memory ≥ <--memory-max> | `free-memory` is at least what `--memory-max` asks for, 64 MiB by default | free memory on the router, or ask for less with `--memory-max` |
| free memory leaves room for the pull | a warning, with `--remote-image` only: `free-memory` is at least `--memory-max` plus 16 MiB, room for RouterOS to pull and extract the image before the agent starts. How much a pull takes is not measured, so the margin is an estimate | install from a tar with `--agent-tar` if the pull fails |
| free flash ≥ <size> (image tar + extracted root) | without `--disk` or `--ephemeral`: `free-hdd-space` is at least twice the image plus 4 MiB. With `--remote-image` nothing is uploaded and the name ends in `(extracted root)`: the root the pulled image is extracted into, 7 MiB, plus 4 MiB | free flash, or install with `--disk tmpfs` or `--ephemeral` where a tmpfs disk exists |
| disk <disk> exists | with `--disk` or `--ephemeral`: a disk with that slot exists | `/disk/add type=tmpfs tmpfs-max-size=64M slot=tmpfs` for a RAM disk, or name an existing disk with `--disk` |
| disk <disk> has ≥ <size> free (image tar + extracted root) | with `--disk` or `--ephemeral`, once the disk exists: its free space is at least twice the image plus 4 MiB; with `--remote-image`, the 7 MiB root plus 4 MiB, as the flash check | free space on that disk, or give a tmpfs disk a larger `tmpfs-max-size` |
| disk tmpfs is RAM | with `--ephemeral`: the disk in slot `tmpfs` is of type `tmpfs` | free the slot for a tmpfs disk, or install with `--disk <slot>` without `--ephemeral` |
| start-on-boot suits a root in RAM | a warning, when the disk is a tmpfs disk: start-on-boot resolves to `no`, since a reboot empties the disk and a container started at boot has no root | pass `--start-on-boot no`, or `--ephemeral` |
| veth name <veth> is free or ours | no veth has that name, or the one that has it carries this install's tag | pick another `--veth` (and `--subnet`), or remove the veth by hand if it is a leftover of yours |
| envlist <name>-env is free or ours | no envlist has that name, or the one that has it holds this install's `MIKROSCOPE_TAG` entry | pick another `--name` |
| install manifest <disk/>mikroscope/<name>.manifest.txt is free or ours | no file is at the install manifest's path, or the one there holds this install's `tag=` line | move the file away, or pick another `--name` |
| container name <name> is free or ours | with `--container-name`: no container has that name, or the one that has it carries this install's tag | pick another `--container-name` |
| subnet <subnet> does not overlap a route | no route of the main table, active or not, lies inside the /30, and no connected network on another interface holds its router end. Routes that only contain the /30 (a default route, a wider prefix to a VPN), blackhole routes and the install's own veth are left out | pick another /30 with `--subnet` |
| interface list <list> exists | the `--iface-list` list (default `LAN`) exists. With `--iface-list none` doctor prints `interface list the veth joins` and passes: no membership is written | `--iface-list none` when no firewall rule needs the veth in a list (doctor offers it first then); otherwise `/interface/list/add name=…`, or pass the list your `in-interface-list=!…` drop rule uses |
| address list <list> | always: install adds the /30 to the `--addr-list` list (default `LANs`), which creates it when it is missing, and uninstall removes the entry. With `--addr-list none` doctor prints `address list the /30 joins`. Whether a rule needs the membership is the next row's question | none |
| no firewall rule drops the agent's replies | doctor reads every enabled rule of the chains the agent's replies meet, `/ip/firewall/raw` prerouting and `/ip/firewall/filter` forward and input, and walks each one as RouterOS does, first match wins, with the replies in the lists the plan joins: no rule drops them. A warning when a rule might, because it matches on something doctor does not judge (a destination, a mark, a rate), and when the replies to a LAN host pass but a rule may drop the ones to the router itself (filter input), which the relay transport needs | the `--iface-list` and `--addr-list` that let the replies through; when no list does (a `src-address=!<range>` rule, say), add an accept rule for `in-interface=<veth>` before that rule, or pick a `--subnet` inside the range |
| no firewall rule doctor reads is invalid or names a deleted list | a warning: no rule of raw prerouting or filter forward and input is marked invalid, which RouterOS passes over as if it were not there (it names an interface that was removed or is not ready, and `about` says which), and none names an interface list that was removed, which RouterOS keeps as the list's id (`in-interface-list=!*2000010`) and matches as an empty list. A rule changed a moment before reads invalid with no reason until RouterOS has applied it | fix or remove what an invalid rule names; set a deleted list again by name, since creating a list of the same name does not repair the rule. With no reason given, run `doctor` again |
| --lan-address <address> is the router's | with `--expose`: an interface of the router holds that address | pass the address the router has on its LAN, as `/ip/address/print` lists it |
| --lan-address is not on the uplink | a warning, with `--expose`: the interface that holds the address carries no default route, is in no `WAN` list, and shares no interface list with the interface that carries the default route | pass the router's LAN address: on the uplink the dst-nat would publish the agent on the Internet side |
| no registry credential meant for another registry | a warning, with `--remote-image` only: no `/container/config` username is set, or the host of `registry-url` is the host the image is pulled from, every spelling of Docker Hub counted as one. An empty `registry-url` with a username set warns. Doctor reads whether a username is set, never the name, and cannot read the password | `/container/config` holds one username for the whole device, and a credential from another registry can make the pull of a public image end in `auth error`. Install from a tar with `--agent-tar`, pass a `--remote-image` on the registry the username belongs to, or clear the username if nothing else needs it |
| the installed agent published on the LAN asks for a token | a warning, shown only when an install of this `--name` has a dst-nat on the LAN: its environment holds a `TOKEN`. Doctor counts the entries, never reads the value | `upgrade` with the same `--name` and `--token <secret>`; or remove the agent, the LAN rules and the container together with `uninstall --name <name> --yes` |
| the router does not answer DNS from its uplink | a warning: `/ip/dns` `allow-remote-requests` is off, or a rule of raw prerouting or filter input drops a UDP query to port 53 that comes in on the interface of the active default route, walked as for the trap check, first match wins. A warning that the queries may pass when a rule matches on something doctor does not judge, a source address list say. IPv6 is not read | a drop rule for UDP and TCP port 53 on the uplink before any accept that takes it, or `/ip/dns/set allow-remote-requests=no` if no LAN host uses the router as its resolver |
| nothing tagged for <name> that these flags do not select | a warning: in every menu an install writes to, the objects that carry the install's tag are no more than the plan for these flags selects. An install made with other flags (`--expose`, other lists, another `--subnet`) leaves more | run `status` and `uninstall` with no shape flag, so that they read the install manifest, or with the flags that install was given |

- **Storage.** Under `install`, the check uses the real tar's size: twice the image, since the tar
  and the root extracted from it share the disk until the tar is deleted, plus 4 MiB. `doctor` on
  its own assumes a 7 MiB image. With `--remote-image` nothing is uploaded, and the check asks for
  the extracted root plus 4 MiB.
- **Registry.** `--remote-image` hands RouterOS the whole reference, registry host included, so the
  device-wide `/container/config registry-url` does not decide where the pull goes, and mikroscope
  neither needs it set nor writes it. `doctor` reads it only for the credential warning above:
  [Troubleshooting](https://docs.jmrp.io/mikroscope/reference/troubleshooting/#one-credential-for-every-registry).
- **Firewall.** The interface-list check and the firewall check exist because of raw rules that
  drop every packet a container sends: [Firewall lists](https://docs.jmrp.io/mikroscope/install/firewall/).

## Health checks

Standalone `doctor`, not the one inside `install`, then asks the agent that is already running what
its ring holds, in a `health` section after the checks. It reads the ring once, directly from this
host, at the address `--subnet` and `--port` give, the one `status` probes; pass `--token` (or
`MIKROSCOPE_TOKEN`) if the agent has one. It never goes through the relay or the `--expose` address,
and it never changes the exit status. It skips the section, saying why, in two cases:

```text
health (what the running agent's ring shows now):
  skipped: no agent answered at 172.30.10.2:9123 from this host (…)
```

when nothing answers `/healthz` within 3 s, and
`skipped: the agent answered /healthz but its ring could not be read (…)` when `/healthz` answers
but the ring does not, as with a token agent read without `--token`.

When it reads the ring, it prints how much it read and either a clean line or one `WARN` per
finding, each with its `fix:`, indented under `health`:

```text
health (what the running agent's ring shows now):
  read 600 samples covering 60 s
  ok    no loop signature, STP churn, link flap or softnet drop in the window
```

```text
  WARN  layer2-loop: 30 frames this router sent came back in on ether2 in the last 60 s, carrying the bridge's own address as their source
        fix: something downstream of ether2 reaches the router by a second path. …
```

The ring is 60 s by default, so this sees what is happening now; the dashboards and the alert rules
read history. It asks the agent for at most 10 000 samples, all within the same 3 s: the whole ring
when it holds no more, otherwise the newest 10 000 (the last 1 000 s at 10 Hz, the last 100 s at
100 Hz). Each finding is a count of events a healthy router does not produce, not a threshold:

| Finding         | Fires on                                                                                                                  |
| --------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `layer2-loop`   | three or more frames in the window that came back in on a port carrying the bridge's own address as their source           |
| `stp-churn`     | a port that STP moved to learning at least three more times than it let it forward, without the loop signature             |
| `link-flap`     | two or more link-downs on one port anywhere in the samples read                                                            |
| `softnet-drops` | any packet the kernel dropped from its softnet backlog                                                                     |

- **`layer2-loop`.** A loop repeats the signature at the STP hello interval, 2 s by
  [RouterOS default](https://help.mikrotik.com/docs/spaces/ROS/pages/21725254/Spanning+Tree+Protocol#SpanningTreeProtocol-Defaultvalues),
  thirty times in a 60 s ring, while RouterOS's counters can show the port healthy and STP keeps it
  blocked ([Layer-2 loop](https://docs.jmrp.io/mikroscope/playbooks/loop/)).
- **`stp-churn`.** Not a count of blocks: a healthy link-up logs three moves to learning at once and
  reaches forwarding seconds later, leaving learning minus forwarding at 0.
- **`link-flap`.** The collector's own `link-flap` detection counts differently: link-ups and
  link-downs together, two or more on one port within 60 s. A cable pulled and plugged back once is
  a flap there and not here.
- **`softnet-drops`.** A packet lost inside the router, where no interface counter sees it.
  Squeezes are not counted: a squeeze is the kernel pacing itself.

Each finding names the RouterOS port on a board the port map knows, and the kernel's name
otherwise. The kernel-log records are classified again from their text, so an older agent is read
the same way. What has run against a live agent is
on [Tested on](https://docs.jmrp.io/mikroscope/about/status/#what-works-end-to-end).

## Your computer

| Need                  | For                                                     | Detail                                                                                   |
| --------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| ssh to the router     | `doctor`, `install`, `status`, `upgrade`, `uninstall`    | an admin user, with a key or an ssh agent                                                |
| `scp`                 | the tar and source routes                               | the image upload                                                                         |
| an agent image        | `install`, `upgrade`                                    | a registry pull, the release tar, or a build from a checkout with Go 1.27                 |
| a route to the agent  | `record`, `forward`, the probe                          | a route to the /30 through the router, the API relay, or `--expose`                     |
| a RouterOS API user   | the relay, `--log-markers`, the collector's API tier    | read-only; it stays on your machine                                                      |

- **ssh.** The CLI runs the system `ssh` with `BatchMode=yes` and `ConnectTimeout=15`, so it cannot
  answer a password or a host-key prompt: use a key or an ssh agent, and for a new router add
  `--ssh-option StrictHostKeyChecking=accept-new`. `--router` takes `user@host` or an ssh config
  alias; `--ssh-port` and `--ssh-key` fall back to your ssh configuration when unset.
- **No ssh at all.** The [RouterOS script](https://docs.jmrp.io/mikroscope/install/script/), the
  [Script generator](https://docs.jmrp.io/mikroscope/install/generator/) and the manual installs run on the router
  itself: [Install methods](https://docs.jmrp.io/mikroscope/install/routes/).
- **The agent.** [Network access](https://docs.jmrp.io/mikroscope/install/reaching-the-agent/) has the three ways to
  reach it, and [API user](https://docs.jmrp.io/mikroscope/security/api-user/) the user's policy.

What has not been tested, such as a board of another architecture or a RouterOS before 7.24, is
under [Not tested](https://docs.jmrp.io/mikroscope/about/status/#not-tested).

## See also

- [Install with the CLI](https://docs.jmrp.io/mikroscope/install/): what `install` does once these are in place.
- [Install methods](https://docs.jmrp.io/mikroscope/install/routes/): the routes the agent image can take to the router.
- [Firewall lists](https://docs.jmrp.io/mikroscope/install/firewall/): why the list and firewall checks exist.
- [Storage and container settings](https://docs.jmrp.io/mikroscope/install/layout/): the disk the storage check is
  about, and `--ephemeral`.
