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
.envas 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.
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
.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
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
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
Environment variables
Pass variables with -e flags or mount an .env file. All are optional with sensible defaults.
.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.| Variable | Default | Description |
|---|---|---|
PORT | 3000 | HTTP 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_PORT | 3443 | HTTPS server port |
STORAGE_ADAPTER | sqlite | Storage back end. Leave at sqlite. |
JWT_SECRET | auto-generated | Secret for JWT tokens. If unset, a random value is generated on first start and persisted to data/.secrets.json. |
ENCRYPTION_KEY | auto-generated | 64-char hex key for AES-256-GCM device password encryption. Same auto-generate and persist behaviour as JWT_SECRET. |
MONITOR_INTERVAL_MS | 60000 | Device polling interval in milliseconds |
MONITOR_CONCURRENCY | 10 | Max devices polled simultaneously |
TLS_ENABLED | false | Enable HTTPS (auto-generates self-signed cert). The value must be exactly true — 1, yes and TRUE are ignored, and the startup log says so. |
TLS_CERT_PATH | — | Path to custom TLS certificate |
TLS_KEY_PATH | — | Path to custom TLS private key |
METRICS_ENABLED | false | Enable the /metrics Prometheus endpoint. Disabled by default. |
METRICS_TOKEN | — | Bearer token for the /metrics endpoint. Optional but recommended; empty = no auth. |
SYSLOG_ENABLED | true | Enable the UDP syslog receiver (Log collector). |
SYSLOG_PORT | 5514 | UDP port for the syslog listener. |
SYSLOG_TCP_ENABLED | true | Enable the TCP syslog receiver (parallel to UDP, same port by default). Useful when UDP is blocked. |
SYSLOG_TCP_PORT | 5514 | TCP port for the syslog listener (defaults to SYSLOG_PORT). |
CVE_ENABLED | true | Enable RouterOS CVE alerting (daily NVD feed match, informational). |
NVD_API_KEY | — | Optional NVD API key — raises the fetch rate limit. Not required. |
CVE_CHECK_INTERVAL_MS | 86400000 | How often to refresh the NVD feed (default 24h). |
WEBAUTHN_RP_ID | — | Registrable 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_ORIGINS | — | Comma-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_LEVEL | info | Log level: debug, info, warn, error |
EXPORT_RSC_ENABLED | false | v1.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). |
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
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).ENCRYPTION_KEY after adding devices, existing stored passwords become unreadable. Save it securely..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
.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
-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:
- Log in as admin
- Navigate to License in the sidebar
- 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
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
| Metric | Description |
|---|---|
mikr_device_up | Device reachability (1 = online, 0 = offline) |
mikr_device_cpu_percent | CPU load percentage |
mikr_device_memory_percent | Memory usage percentage |
mikr_device_memory_free_bytes | Free memory in bytes |
mikr_device_memory_total_bytes | Total memory in bytes |
mikr_device_uptime_seconds | Device uptime in seconds |
mikr_device_temperature_celsius | Board temperature |
mikr_device_voltage_volts | Input voltage |
mikr_device_power_watts | Power consumption |
mikr_interface_rx_bits_per_second | Per-interface receive rate in bits per second |
mikr_interface_tx_bits_per_second | Per-interface transmit rate in bits per second |
mikr_device_info | Device metadata (RouterOS version, model, architecture) |
mikr_devices_total | Total number of enabled devices |
mikr_devices_online | Number 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:
- Download grafana-dashboard.json
- In Grafana, go to Dashboards → Import → Upload JSON file
- 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.
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.
- Download zabbix-template.yaml
- 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
- In Zabbix, go to Data collection → Templates → Import and upload the YAML file
- 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.
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:
- Role —
viewer,operator, oradmin(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
| Role | Read | Run operations | Change config & inventory | Manage users & keys |
|---|---|---|---|---|
viewer | Yes | — | — | — |
operator | Yes | Yes | — | — |
admin | Yes | Yes | Yes | — |
| superadmin * | Yes | Yes | Yes | Yes |
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.
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— andrebootas well if you upgrade or restart devices from the manager - For REST devices, ensure
www-sslorwwwservice 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.