Documentation / Installation

Installation

Install MikroTik Manager using Docker, Docker Compose, or as a MikroTik Container App on the router itself.

Requirements

  • Docker Engine on Linux, macOS, or Windows with WSL2 — any release still supported by Docker will do, because the version floor below is set by Compose rather than by the engine
  • Docker Compose v2.24 or newer, if you install that way — the compose file below declares its .env as optional, which older versions do not understand
  • Around 300 MB of disk for the image itself, and 512 MB of RAM — a running manager sits near 170 MB with a small fleet
  • Room for the data directory on top of that. It holds the database, the certificates and the backups, and it grows with log retention and metric history, so a fleet of any size wants a few gigabytes rather than a few hundred megabytes
  • Network access to your MikroTik devices (SSH port 22 and/or HTTPS port 443)

Installing with Docker

Step 1 — Pull the image

docker pull ghcr.io/hreskiv/mikr:latest

Step 2 — Create a data directory

This directory stores the SQLite database, TLS certificates, and backups. It persists across container restarts and updates.

mkdir -p /opt/mikr/data

Step 3 — Run the container

docker run -d \
  --name mikr-manager \
  --restart unless-stopped \
  -p 3000:3000 \
  -p 3443:3443 \
  -p 5514:5514/udp \
  -p 5514:5514/tcp \
  -v /opt/mikr/data:/app/data \
  -e STORAGE_ADAPTER=sqlite \
  ghcr.io/hreskiv/mikr:latest

Port 5514 is the optional syslog receiver (Log collector). Both UDP and TCP listeners run by default — RouterOS 7.x can send over either. Skip the -p 5514:5514/... lines if you don't need logs, or set SYSLOG_ENABLED=false / SYSLOG_TCP_ENABLED=false.

Step 4 — Open in browser

Navigate to http://your-server-ip:3000 and log in.

The first start creates the admin account on its own — there is no separate seed step. Default credentials are admin / admin. Change the password immediately after first login.

Installing with Docker Compose

If you prefer Docker Compose, create these two files and run one command.

Step 1 — Create project directory

mkdir -p /opt/mikr && cd /opt/mikr

Step 2 — Create the .env file

This file stores your secrets. Generate random values and save them — you'll need the same keys on every update.

cat > /opt/mikr/.env <<EOF
JWT_SECRET=$(openssl rand -hex 32)
ENCRYPTION_KEY=$(openssl rand -hex 32)
EOF
Back up .env immediately. If ENCRYPTION_KEY is lost, all stored device passwords become unreadable.

Step 3 — Create docker-compose.yml

cat > /opt/mikr/docker-compose.yml <<'EOF'
services:
  mikr:
    image: ghcr.io/hreskiv/mikr:latest
    container_name: mikr-manager
    restart: unless-stopped
    ports:
      - "3000:3000"
      - "3443:3443"
      - "5514:5514/udp"   # optional — syslog receiver UDP
      - "5514:5514/tcp"   # optional — syslog receiver TCP
    volumes:
      - ./data:/app/data
    environment:
      - STORAGE_ADAPTER=sqlite
    env_file:
      - path: .env
        required: false
EOF

Step 4 — Start

cd /opt/mikr
docker compose up -d
Default credentials are admin / admin. Change the password immediately after first login.

MikroTik Container (RouterOS 7.22+)

Run mikr directly on a MikroTik router using the Container feature — no separate server needed.

Requirements

  • RouterOS 7.22+ with Container package installed
  • ARM64 (RB5009, hAP ax², cAP ax) or x86 (CHR) architecture
  • At least 512 MB of free RAM, and around 300 MB of free storage for the image before any data
  • USB or external storage recommended for persistence

Container App YAML

Paste this into the YAML tab of the MikroTik Container App configuration:

name: mikr
descr: MikroTik Manager — web-based device management, monitoring, and configuration for MikroTik fleets.
page: https://mikr.app
category: networking
default-credentials: admin/admin
services:
  mikr:
    image: ghcr.io/hreskiv/mikr:latest
    ports:
      - "3000:3000:web"
      - "3443:3443:web-ssl"
      - "5514:5514/udp:syslog-udp"
      - "5514:5514/tcp:syslog-tcp"
    environment:
      - HOST=0.0.0.0
      - PORT=3000
      - ENCRYPTION_KEY=<your-encryption-key>
      - JWT_SECRET=<your-jwt-secret>
    volumes:
      - data:/app/data
    restart: unless-stopped
Generate keys with openssl rand -hex 32 before pasting. The admin user is created automatically on first start — no seed command needed.

RouterOS CLI (alternative)

If you prefer configuring via CLI instead of the Container App UI:

# Enable container mode (requires reboot)
/system/device-mode/update container=yes

# Create veth interface for the container
/interface/veth/add name=veth-mikr address=172.17.0.2/24 gateway=172.17.0.1

# Create a bridge for containers
/interface/bridge/add name=bridge-containers
/interface/bridge/port/add bridge=bridge-containers interface=veth-mikr
/ip/address/add address=172.17.0.1/24 interface=bridge-containers

# NAT for container internet access
/ip/firewall/nat/add chain=srcnat action=masquerade src-address=172.17.0.0/24

# Port forwarding — access mikr from LAN
/ip/firewall/nat/add chain=dstnat action=dst-nat protocol=tcp \
    dst-port=3000 to-addresses=172.17.0.2 to-ports=3000

# (optional) Syslog receiver — lets other MikroTiks send logs to mikr
/ip/firewall/nat/add chain=dstnat action=dst-nat protocol=udp \
    dst-port=5514 to-addresses=172.17.0.2 to-ports=5514

# (optional) TCP syslog — useful when UDP is blocked in the path
/ip/firewall/nat/add chain=dstnat action=dst-nat protocol=tcp \
    dst-port=5514 to-addresses=172.17.0.2 to-ports=5514

# Persistent data mount (use USB/NVMe, not NAND flash)
/container/mounts/add list=mikr-data src=disk1/mikr/data dst=/app/data

# Environment variables
/container/envs/add list=mikr-env key=HOST value=0.0.0.0
/container/envs/add list=mikr-env key=PORT value=3000
/container/envs/add list=mikr-env key=ENCRYPTION_KEY value=<your-key>
/container/envs/add list=mikr-env key=JWT_SECRET value=<your-secret>

# Pull and create container
/container/add remote-image=ghcr.io/hreskiv/mikr:latest \
    interface=veth-mikr envlist=mikr-env mountlists=mikr-data \
    hostname=mikr start-on-boot=yes logging=yes

# Start
/container/start 0
Use USB or NVMe storage for the data mount — internal NAND flash has limited write cycles. mikr can monitor the router it runs on via the bridge IP (172.17.0.1).

Environment variables

Pass variables with -e flags or mount an .env file. All are optional with sensible defaults.

From v1.30.0, most of these are also editable from Settings → System configuration without a compose edit or restart — CVE feed, retention windows, monitor cadence, JWT expiry, WebAuthn RP config, log level, default ports for new devices, the .rsc mirror, and more. Anything you set as an env var here still wins (it shows as locked in the UI with a "from env" badge), so existing deployments keep working unchanged. Bootstrap-critical values (PORT, HOST, JWT_SECRET, ENCRYPTION_KEY, TLS_*, SYSLOG_PORT) stay env-only by design.
VariableDefaultDescription
PORT3000HTTP server port
HOST::Address to bind. The default accepts IPv4 and IPv6; set 0.0.0.0 where IPv6 is not available, as the Container examples above do.
HTTPS_PORT3443HTTPS server port
STORAGE_ADAPTERsqliteStorage back end. Leave at sqlite.
JWT_SECRETauto-generatedSecret for JWT tokens. If unset, a random value is generated on first start and persisted to data/.secrets.json.
ENCRYPTION_KEYauto-generated64-char hex key for AES-256-GCM device password encryption. Same auto-generate and persist behaviour as JWT_SECRET.
MONITOR_INTERVAL_MS60000Device polling interval in milliseconds
MONITOR_CONCURRENCY10Max devices polled simultaneously
TLS_ENABLEDfalseEnable HTTPS (auto-generates self-signed cert). The value must be exactly true1, yes and TRUE are ignored, and the startup log says so.
TLS_CERT_PATHPath to custom TLS certificate
TLS_KEY_PATHPath to custom TLS private key
METRICS_ENABLEDfalseEnable the /metrics Prometheus endpoint. Disabled by default.
METRICS_TOKENBearer token for the /metrics endpoint. Optional but recommended; empty = no auth.
SYSLOG_ENABLEDtrueEnable the UDP syslog receiver (Log collector).
SYSLOG_PORT5514UDP port for the syslog listener.
SYSLOG_TCP_ENABLEDtrueEnable the TCP syslog receiver (parallel to UDP, same port by default). Useful when UDP is blocked.
SYSLOG_TCP_PORT5514TCP port for the syslog listener (defaults to SYSLOG_PORT).
CVE_ENABLEDtrueEnable RouterOS CVE alerting (daily NVD feed match, informational).
NVD_API_KEYOptional NVD API key — raises the fetch rate limit. Not required.
CVE_CHECK_INTERVAL_MS86400000How often to refresh the NVD feed (default 24h).
WEBAUTHN_RP_IDRegistrable domain for passkey / WebAuthn sign-in (e.g. mikr.example.com). Must be a real domain, not an IP address — the WebAuthn spec forbids IP RP IDs. Leave unset on LAN-IP / non-HTTPS installs; the Manager keeps working and the Settings page shows a clear "Passkey — Unavailable" notice.
WEBAUTHN_ORIGINSComma-separated list of allowed origins for passkey ceremonies (e.g. https://mikr.example.com). Defaults to https://<WEBAUTHN_RP_ID> if unset but WEBAUTHN_RP_ID is set.
LOG_LEVELinfoLog level: debug, info, warn, error
EXPORT_RSC_ENABLEDfalsev1.30.0+. When true, mirror the latest .rsc script export of every device to exports/<site>/<device>.rsc inside the data directory after each backup. One file per device, overwritten on each new backup (see the note below).
On first start with no JWT_SECRET / ENCRYPTION_KEY set, the Manager generates strong random values and saves them to data/.secrets.json (mode 0600). Back up this file — losing it invalidates all sessions and makes stored device passwords unrecoverable. To use your own values, set them via env or .env (env always wins); generate with openssl rand -hex 32.

Example with custom secrets:

docker run -d \
  --name mikr-manager \
  --restart unless-stopped \
  -p 3000:3000 \
  -p 3443:3443 \
  -v /opt/mikr/data:/app/data \
  -e STORAGE_ADAPTER=sqlite \
  -e JWT_SECRET=$(openssl rand -hex 32) \
  -e ENCRYPTION_KEY=$(openssl rand -hex 32) \
  ghcr.io/hreskiv/mikr:latest
Passkey / WebAuthn sign-in (v1.28.0+) only activates when both WEBAUTHN_RP_ID and WEBAUTHN_ORIGINS are set and the Manager is served over HTTPS on the matching domain (e.g. via a Caddy / nginx reverse-proxy with Let's Encrypt). LAN-IP installs work fine without it — Settings just shows "Passkey — Unavailable" with an explanation. Changing WEBAUTHN_RP_ID later invalidates every previously registered passkey (a WebAuthn spec property).
If you change ENCRYPTION_KEY after adding devices, existing stored passwords become unreadable. Save it securely.
Optional .rsc mirror (v1.30.0+). With the .rsc export switched on, mikr writes one current .rsc per device under exports/<site>/<device>.rsc inside its data directory — /app/data/exports in the container, which is /opt/mikr/data/exports on the host if you followed the commands above. It is part of the volume you already mount, so no second volume is needed. Point a git checkout at that path and commit after each write for a free change history, or rclone it to a NAS / cloud for 3-2-1 backups.

Updating

Your data is stored in /opt/mikr/data (mounted volume), so it survives container replacement.

Docker Compose

cd /opt/mikr
docker compose pull
docker compose up -d
Docker Compose automatically reads .env — your secrets persist across updates without extra flags.

Plain Docker

# Pull the latest image
docker pull ghcr.io/hreskiv/mikr:latest

# Stop and remove the old container
docker stop mikr-manager
docker rm mikr-manager

# Start a new container with the same settings
docker run -d \
  --name mikr-manager \
  --restart unless-stopped \
  -p 3000:3000 \
  -p 3443:3443 \
  -v /opt/mikr/data:/app/data \
  --env-file /opt/mikr/.env \
  ghcr.io/hreskiv/mikr:latest
If you used -e flags instead of --env-file, include them again in the docker run command. See Environment variables for the recommended .env file approach.

Clean up old images

docker image prune -f

Activating a licence

The Community edition supports up to 10 devices for free. To manage more devices, activate your licence key in the app:

  1. Log in as admin
  2. Navigate to License in the sidebar
  3. Paste your licence key and click Activate

The licence is stored in the database and persists across updates.

Enabling HTTPS

Auto-generated self-signed certificate

docker run -d \
  --name mikr-manager \
  --restart unless-stopped \
  -p 3000:3000 \
  -p 3443:3443 \
  -v /opt/mikr/data:/app/data \
  -e STORAGE_ADAPTER=sqlite \
  -e TLS_ENABLED=true \
  ghcr.io/hreskiv/mikr:latest

Access via https://your-server-ip:3443. Your browser will warn about the self-signed cert — this is expected.

Custom certificate

docker run -d \
  --name mikr-manager \
  --restart unless-stopped \
  -p 3000:3000 \
  -p 3443:3443 \
  -v /opt/mikr/data:/app/data \
  -v /path/to/certs:/certs:ro \
  -e STORAGE_ADAPTER=sqlite \
  -e TLS_ENABLED=true \
  -e TLS_CERT_PATH=/certs/fullchain.pem \
  -e TLS_KEY_PATH=/certs/privkey.pem \
  ghcr.io/hreskiv/mikr:latest
With TLS_ENABLED=true, both HTTP (port 3000) and HTTPS (port 3443) run in parallel. WebSocket (WS/WSS) works automatically on both. The value has to be the exact word true; anything else leaves the Manager serving HTTP only and records the reason in its startup log.

Prometheus, Grafana & Zabbix

mikr exposes a /metrics endpoint in Prometheus text format. Connect it to Prometheus + Grafana for historical graphs of CPU, memory, temperature, per-interface traffic, and device availability.

Enable the endpoint

The /metrics endpoint is disabled by default (returns 404). You can turn it on in the app under Settings → Prometheus export, where you can also set an optional scrape token. To enable it with environment variables instead, set METRICS_ENABLED=true:

# In .env file
METRICS_ENABLED=true

# Optional but recommended — Bearer token auth
METRICS_TOKEN=your-secret-token

Generate a strong random token:

openssl rand -hex 32

If METRICS_TOKEN is set, Prometheus must send a Bearer token or use ?token=your-secret-token.

Verify metrics are available

curl http://your-server-ip:3000/metrics

Prometheus configuration

Add this to your prometheus.yml:

scrape_configs:
  - job_name: 'mikr'
    scrape_interval: 60s
    metrics_path: /metrics
    static_configs:
      - targets: ['your-server-ip:3000']

With token authentication:

scrape_configs:
  - job_name: 'mikr'
    scrape_interval: 60s
    metrics_path: /metrics
    authorization:
      type: Bearer
      credentials: 'your-secret-token'
    static_configs:
      - targets: ['your-server-ip:3000']

If mikr uses HTTPS (port 3443):

scrape_configs:
  - job_name: 'mikr'
    scheme: https
    tls_config:
      insecure_skip_verify: true
    scrape_interval: 60s
    metrics_path: /metrics
    static_configs:
      - targets: ['your-server-ip:3443']

Available metrics

MetricDescription
mikr_device_upDevice reachability (1 = online, 0 = offline)
mikr_device_cpu_percentCPU load percentage
mikr_device_memory_percentMemory usage percentage
mikr_device_memory_free_bytesFree memory in bytes
mikr_device_memory_total_bytesTotal memory in bytes
mikr_device_uptime_secondsDevice uptime in seconds
mikr_device_temperature_celsiusBoard temperature
mikr_device_voltage_voltsInput voltage
mikr_device_power_wattsPower consumption
mikr_interface_rx_bits_per_secondPer-interface receive rate in bits per second
mikr_interface_tx_bits_per_secondPer-interface transmit rate in bits per second
mikr_device_infoDevice metadata (RouterOS version, model, architecture)
mikr_devices_totalTotal number of enabled devices
mikr_devices_onlineNumber of online devices

All per-device metrics include labels: name, host, site. The interface traffic metrics add an interface label. The rate metrics already carry a computed bits-per-second value, so you graph them directly without rate().

InfluxDB (via Telegraf)

There is no native InfluxDB push. Point a Telegraf prometheus input at the endpoint and Telegraf writes the series into InfluxDB:

[[inputs.prometheus]]
  urls = ["http://your-server-ip:3000/metrics"]
  # http_headers = { "Authorization" = "Bearer your-secret-token" }

[[outputs.influxdb_v2]]
  urls = ["http://influxdb:8086"]
  token = "INFLUX_TOKEN"
  organization = "your-org"
  bucket = "mikrotik"

Grafana dashboard

Import the ready-made dashboard template:

  1. Download grafana-dashboard.json
  2. In Grafana, go to Dashboards → Import → Upload JSON file
  3. Select your Prometheus data source and click Import

The dashboard includes: fleet overview stats, device status table, CPU/memory time series, temperature/voltage/power graphs, per-interface traffic (RX/TX), uptime tracking, RouterOS version distribution, and device model breakdown. Filter by site and device using the dropdown variables at the top.

mikr polls devices every 60 seconds. Set Prometheus scrape_interval to 60s to match — scraping more frequently won't provide additional data.

Zabbix

Zabbix does not need the /metrics endpoint at all. The template reads the REST API, so it also sees what /metrics does not publish: CVE counts, missing security releases, licence headroom, syslog throughput, blocked attackers and failed tasks. Your routers need no SNMP community and no Zabbix agent, and do not have to be reachable from the Zabbix server — everything comes from the manager.

  1. Download zabbix-template.yaml
  2. In the manager, sign in as a Super Admin, open API Keys in the sidebar and create a key with the viewer role and access to every site
  3. In Zabbix, go to Data collection → Templates → Import and upload the YAML file
  4. Link the template to a host and fill in two macros:
    • {$MIKR.URL}https://mikr.example.com, no trailing slash
    • {$MIKR.API.KEY} — the key from step 2

Devices are discovered from the API, so every device the manager polls arrives with its own items and triggers: online state, CPU, memory, temperature, voltage, power, RouterOS version, RouterBOARD firmware, and how long ago it was last polled. Triggers cover offline devices, threshold breaches, an available RouterOS update, firmware left behind after an upgrade, and monitoring data going stale.

Thresholds are macros — {$MIKR.CPU.WARN}, {$MIKR.TEMP.WARN}, {$MIKR.RAM.WARN} — and take a per-device context, so {$MIKR.CPU.WARN:"core-router"} overrides the fleet default for one device. {$MIKR.DEVICE.MATCHES} and {$MIKR.DEVICE.NOT_MATCHES} limit discovery to devices whose name matches a pattern.

For the first hour after import, the RouterOS version changed triggers sit in an unknown state reading cannot get values from value cache. That is expected and clears itself: the check needs two values, and version items only store one when the version actually changes or an hour goes by.

API access

MikroTik Manager exposes an HTTP/JSON API under /api — the same one the web interface uses. Scripts, monitoring, and CI can call it using an API key, with no interactive login or token refresh.

Creating a key

Sign in as a Super Admin, open the API Keys page, and click Create API Key. Choose:

  • Roleviewer, operator, or admin (a key can never be Super Admin)
  • Scope — optionally limit the key to specific sites, exactly like a scoped user
  • Expiry — optional; leave empty for a key that never expires

The token (format mikr_…) is shown once at creation — copy it then, it cannot be retrieved later. Only a SHA-256 hash is stored on the server.

Using a key

Send the key in an X-API-Key header:

curl -H "X-API-Key: mikr_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  https://YOUR-HOST:3443/api/devices

…or as a Bearer token (the mikr_ prefix is auto-detected):

curl -H "Authorization: Bearer mikr_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  https://YOUR-HOST:3443/api/devices

Roles

RoleReadRun operationsChange config & inventoryManage users & keys
viewerYes
operatorYesYes
adminYesYesYes
superadmin *YesYesYesYes

Run operations — execute commands and templates, RouterOS/firmware upgrades, reboots, create and download backups, test/refresh devices. Change config & inventory — create/edit/delete devices and sites, settings, command templates, webhooks, IDS, license, DHCP and service toggles.

A key acts with its role across the same server-side role and per-site checks as a user. * Super Admin (manages users, API keys, and per-site access grants) is available in the interface only — an API key can never hold it.

Revoke or delete a key from the API Keys page to disable it instantly. Key creation, revocation, and API calls made with a key are recorded in the Activity log.

Troubleshooting

View logs

docker logs mikr-manager --tail 100

Follow logs in real-time

docker logs mikr-manager -f

Enable debug logging

Stop the container and re-run with -e LOG_LEVEL=debug to see SSH/REST connection details.

Container won't start

  • Check if port 3000 is already in use: ss -tlnp | grep 3000
  • Verify the data directory exists and is writable
  • Check Docker logs for startup errors

Devices show "Error" or "Offline"

  • Verify SSH connectivity from the Docker host: ssh admin@device-ip
  • Check that the MikroTik user has required permissions: ssh, read, write, sensitive, api, rest-api — and reboot as well if you upgrade or restart devices from the manager
  • For REST devices, ensure www-ssl or www service is enabled on the MikroTik
  • Check container logs for specific error messages

Reset admin password

Try the app first. A second Super Admin can set another account's password from the Users page, and every user changes their own under Settings → Password. The command below is for the case where nobody can sign in at all.

If the account is simply missing, re-run the seed script — it only creates a new admin when there is no user at all, and says so and stops otherwise:

docker exec mikr-manager node scripts/seed.js

To force a new password onto an existing account, run this against the container. Replace newpassword with the one you want, and admin with the username if it differs:

docker exec mikr-manager node -e "
const {getStore}=require('./src/data/store');
const {hashPassword}=require('./src/services/auth.service');
(async()=>{
  const store=getStore();
  const user=await store.users.findByUsername('admin');
  if(!user) return console.error('no such user');
  await store.users.update(user.id,{
    passwordHash: await hashPassword('newpassword'),
    passwordChangedAt: Date.now()
  });
  console.log('password reset for', user.username);
})();"

Every other session signed in as that account is signed out shortly afterwards, on this machine and any other. Sign in with the new password and change it from Settings once you are back in.