How to run a MikroTik NetFlow collector with Docker Compose and keep it deployed
A MikroTik router will tell you exactly which device is using the connection, if something collects the flow records it exports. The open-source mikrotik-home-netflow-plus collector does that in one container: live throughput, per-device history, a flow map, alerts and a wall display. This guide configures the router, runs the collector with plain Compose, then turns it into a Simple Docker Ops application with an encrypted secret, digest-pinned releases, a health gate, upgrades and rollback.
What you get
RouterOS 7 can export a record of every connection the router forwards (Traffic Flow, as NetFlow or IPFIX) and expose its live state over a read-only API. mikrotik-home-netflow-plus, Apache 2.0 licensed, combines the two: one Go binary, one container, one SQLite file, and a web interface with a live throughput chart, a page per device, a Sankey flow map, nine alert rules with a webhook, and a full-screen wall display. The published image is built for amd64 and arm64, so a Raspberry Pi 4 or 5 next to the router is as good a host as an x86 server.
Before you start
- A MikroTik router on RouterOS 7 and a terminal on it: WinBox › New Terminal, WebFig › Terminal, or SSH.
- A Linux host on the router's LAN with Docker Engine 24 or newer and the Compose v2 plugin, running as a systemd service. Raspberry Pi OS, Debian, Ubuntu or Armbian on a Pi 4 or 5, a mini PC or a VM.
- A static address or a static DHCP lease for that host. The router will export to it and restrict the API user to it.
- For step 4, a free Simple Docker Ops account.
When you are done
- The collector running as a release: image pinned to its digest, health-checked, restarted by the agent if it fails.
- The router password encrypted in the portal, delivered to the agent only with a release, never in a file you manage on the host.
- Upgrades and rollback from the portal or the API, without SSH or an inbound port.
- Optionally, the wall display on a Raspberry Pi screen.
| Lane | Source | Delay | Gives you |
|---|---|---|---|
| Accounting lane | IPFIX flow records the router pushes over UDP 2055 | 15 to 75 seconds | History, totals, rankings, the flow map, alerts. Every flow the router forwarded, FastTracked ones included. |
| Live lane (optional) | The RouterOS API, polled with a read-only user over TLS on port 8729 | 1 to 2 seconds | Current rates, the active connection table, device names from DHCP, destination names from the router's DNS cache. |
Step 1Configure the router
Two lines turn on flow export. The read-only API user and its certificate are optional but worth the extra minute: without them the collector still keeps history and raises alerts, but live views show a 90-second average and devices are named by MAC address. Replace <COLLECTOR_IP> with the host's address and <ROUTER_LAN_IP> with the router's LAN address.
Send flow records to the host
active-flow-timeout=1mis the lowest value RouterOS accepts and the one the collector expects; the default of 30 minutes would hide a long download for half an hour. FastTrack can stay on.RouterOS terminal /ip traffic-flow set enabled=yes active-flow-timeout=1m /ip traffic-flow target add dst-address=<COLLECTOR_IP> port=2055 version=ipfix \ src-address=<ROUTER_LAN_IP> v9-template-refresh=20 v9-template-timeout=1mAdd a read-only API user
The
flowmongroup can read and use the API, nothing else, and the account only works from the host's address. The collector never needs an administrative account.RouterOS terminal /user group add name=flowmon policy=read,api /user add name=flowmon group=flowmon password=<STRONG_PASSWORD> address=<COLLECTOR_IP>/32Give the API a certificate
The encrypted API service (
api-ssl, port 8729) has no certificate out of the box. A self-signed one is enough; the collector pins it on first use. Wait for the sign command to finish before the last line.RouterOS terminal /certificate add name=api-cert common-name=router.lan key-size=2048 days-valid=3650 \ key-usage=key-cert-sign,crl-sign,digital-signature,key-encipherment,tls-server /certificate sign api-cert /ip service set api-ssl certificate=api-cert/ip traffic-flow target printshows one target pointing at the host, and/ip service print where name=api-sslshowsapi-cert.
The project's router setup guide explains every setting, the IPFIX fields, running without TLS on port 8728, keeping the router clock right, and how to undo it all. Once the collector runs, its Status page shows this whole script with your addresses filled in.
Step 2Prepare the host
Check Docker
The collector needs Docker and Compose v2; the Simple Docker Ops agent additionally needs Docker running as the systemd service
docker.service. Docker from snap, rootless Docker and Podman do not qualify; install Docker Engine from Docker's own repository (the supported devices page has the commands).shell docker version --format '{{.Server.Version}}' # 24 or newer docker compose version --short # 2 or newer (Docker's repository ships 5.x; the major is what counts) systemctl is-active docker # activeCheck the ports are free
The collector runs with host networking and binds UDP 2055 and TCP 8080 directly. If something already holds 8080, pick another port now and use it in step 3 and step 4.
shell sudo ss -lntup | grep -E ':(8080|2055)\b' # no output means both are freePin the host's address
A static address, or a static lease on the router (
/ip dhcp-server lease make-static). The router's export target and theflowmonuser's address restriction both point at it.
Step 3Run it with plain Compose
This is the repository's own quick start and it works on any Docker host. Do it once even if you plan to use Simple Docker Ops: it proves the router side before the platform is involved.
Clone and configure
shell git clone https://github.com/thedyerman/mikrotik-home-netflow-plus.git cd mikrotik-home-netflow-plus cp deploy/env.example deploy/.env && chmod 600 deploy/.envEdit
deploy/.env. The five lines that matter:deploy/.env NFP_EXPORTERS=192.168.88.1 # the router's LAN address: packets from anywhere else are dropped NFP_ROUTER_ADDR=192.168.88.1 # same address; leave empty to run from flow records only NFP_ROUTER_USER=flowmon NFP_ROUTER_PASSWORD=<STRONG_PASSWORD> NFP_ROUTER_TLS=trueThe compose file it belongs to is short. Host networking, a named volume for the database, and the environment from the file:
deploy/docker-compose.yml services: netflow: image: kcdyer/mikrotik-home-netflow-plus:1.1.0 container_name: mikrotik-home-netflow-plus restart: unless-stopped network_mode: host env_file: .env volumes: - netflow-data:/data volumes: netflow-data:Start it
shell docker compose -f deploy/docker-compose.yml up -dCheck the Status page
Open
http://<host>:8080and click Status. The indicator at the bottom of the sidebar says Live when both lanes work and Delayed when only flow records arrive.
| Card | What you want to see |
|---|---|
| Flow export | "Last export just now", records being decoded, none lost |
| Router API | "Connected", with the router's model and RouterOS version |
| Flow coverage | Above 95% after about five minutes of traffic |
| Network | Your LAN under local networks and the right interface under WAN interfaces |
A working collector. Stop here if one machine, updated by hand, is all you need. Keep going to hand the lifecycle to Simple Docker Ops.
Step 4Make it a Simple Docker Ops application
The difference from step 3 is where the configuration lives. There is no .env file on the host: every ${NAME} in the compose text becomes a variable of the application, the password becomes a secret, and the agent writes what the container needs on the device at deploy time. The image is public, so no registry credentials are involved.
Pair the host
On the host, run the installer. It checks Docker, installs the agent as a systemd service and prints Pair this device: with a code such as
X72K-4PQ9. The code is single-use and valid for 15 minutes.shell, on the host curl -fsSL https://simpledockerops.com/install | sudo shIn the portal click + Add device, enter the code under Enrollment code, keep the host name or give it one, leave Fleet at No fleet for a single server, and click Enroll device. The agent connects outbound only; nothing new listens on the host.
The device appears on the Devices page as online, with its architecture and Docker version.
Stop the plain Compose stack
Both would bind the same ports. Run this on the host before the first deployment. The release below starts with a fresh database; the collector fills it again within minutes.
shell, on the host docker compose -f deploy/docker-compose.yml downCreate the application
Applications+ New application. Name
netflow, paste the text below into docker-compose.yml, and Create. It is the repository'sdeploy/simpledockerops-compose.yml: the same container, with every setting as a variable instead of a file.docker-compose.yml, pasted into the application services: netflow: image: kcdyer/mikrotik-home-netflow-plus:1.1.0 restart: unless-stopped network_mode: host environment: NFP_EXPORTERS: ${ROUTER_IP} NFP_ROUTER_ADDR: ${ROUTER_IP} NFP_ROUTER_USER: ${ROUTER_USER} NFP_ROUTER_PASSWORD: ${ROUTER_PASSWORD} NFP_ROUTER_TLS: "true" NFP_SITES: ${SITES} NFP_HTTP_LISTEN: ":${HTTP_PORT}" volumes: - netflow-data:/data volumes: netflow-data:Fill in the variables
The portal detects the
${…}names and lists them in the application's Variables card with a Default, a Required flag and a Secret flag.ROUTER_PASSWORDis marked secret automatically because of its name; its default is disabled and says "set under Secrets". Fill in the others and Save working copy.Store the router password as a secret
SecretsSet a secret: Scope
Application, Whichnetflow, NameROUTER_PASSWORD, Value theflowmonpassword, Save secret. It is encrypted at rest, shown as dots from now on, and delivered to the agent only when a release needs it.Cut a release
Back on the application page, Cut release. Version
1.1.0, Notes as you like. The portal resolves the image to its digest and records the platforms it provides; the preflight notes would warn if there were nolinux/arm64build.The release shows as ready with
linux/amd64andlinux/arm64and the frozen compose text with the image pinned by@sha256:.Deploy it
ReleasesDeploy on the release. Target
One device, choose the host, Start rollout. (The device page has the same thing as Choose a release to deploy… and Deploy to this device.) The agent pulls the image, starts the container and reports healthy once the container is running and its built-in health check has passed for 30 seconds.The deployment shows the device as healthy.
http://<host>:8080opens the collector and its Status page reads as in step 3.
| Variable | Value | Secret |
|---|---|---|
ROUTER_IP | The router's LAN address, for example 192.168.88.1 | no |
ROUTER_USER | flowmon | no |
ROUTER_PASSWORD | Set under Secrets, never as a default | yes |
SITES | Optional names for remote sites, for example office=192.168.99.0/24. Leave empty if you have none | no |
HTTP_PORT | 8080, or another free port | no |
The application's defaults are frozen into each release. Fleet variables in the fleet editor and Device variables on the device page override them, and saving either re-pushes the current release straight away. That is how one application serves several routers: the same release everywhere, a different ROUTER_IP per device, and a ROUTER_PASSWORD secret scoped to each device.
No API user on the router? Remove the three NFP_ROUTER_* lines from the compose text and skip ROUTER_USER and ROUTER_PASSWORD. Keep NFP_EXPORTERS: it is what makes the collector accept the router's packets.
Upgrades and rollback
Upgrade
In the application, change the image tag in the compose text to the new version, Save working copy, Cut release with that version, deploy it. The
netflow-datavolume is kept, and open browser tabs and wall displays notice the new version and reload by themselves.Roll back
On the deployment page, Roll back returns every device the deployment touched to the release it was running before, in one wave. Deploying any earlier release again does the same. In a fleet rollout, On failure set to Roll back affected devices makes a release that never turns healthy roll back by itself.
The compose project on the device is named after the application's slug (sdko-netflow), and so is the volume (sdko-netflow_netflow-data). Renaming the application keeps its slug. Deleting it and creating a new one gives a new slug, a new volume and an empty database. Upgrade by cutting new releases of the same application.
Your own build
The published image covers normal use. To run a modified version, build it and push it somewhere the device can pull from. The managed registry gives every organisation private repositories under its own slug at registry.simpledockerops.com (one gigabyte on the Free plan). Devices pull from it with a credential of their own, so there is no docker login to run on the host.
docker login registry.simpledockerops.com # portal account, a registry password, or an API key
docker buildx build --platform linux/amd64,linux/arm64 --build-arg VERSION=1.1.0 \
-t registry.simpledockerops.com/<your-org>/mikrotik-home-netflow-plus:1.1.0 --push .Then put that image name in the compose text and cut a release. Docker Hub, GHCR and any other OCI registry work too: add the credential under RegistryExternal registries and it is used to resolve the digest at release time and for the device's pull. Build for the architecture of the host, or for both as above.
Automate with the API
Everything above can be scripted with an API key from DevelopersAPI keys. The key starts with sdko_, is shown once, and acts with admin rights, so keep it in your CI system's secret store.
# Cut a release, then deploy it to one device
curl -X POST https://app.simpledockerops.com/api/v1/applications/APP_ID/releases \
-H "Authorization: Bearer $SDKO_KEY" -H "Content-Type: application/json" \
-d '{"version":"1.1.0","notes":"first release"}'
curl -X POST https://app.simpledockerops.com/api/v1/deployments \
-H "Authorization: Bearer $SDKO_KEY" -H "Content-Type: application/json" \
-d '{"release_id":"RELEASE_ID","target_type":"device","target_id":"DEVICE_ID",
"strategy":{"waves":[100],"min_healthy_s":30,"on_failure":"rollback"}}'
# Watch it, or roll it back
curl https://app.simpledockerops.com/api/v1/deployments/DEPLOYMENT_ID -H "Authorization: Bearer $SDKO_KEY"
curl -X POST https://app.simpledockerops.com/api/v1/deployments/DEPLOYMENT_ID/rollback -H "Authorization: Bearer $SDKO_KEY"waves are cumulative percentages of the target; for a single device they are always [100]. min_healthy_s is how long the container must stay healthy before the device counts, and on_failure is pause or rollback. The API reference has the full shape.
Put it on a wall display
The collector's /dashboard page is a full-screen wall display built for a small always-on screen: download and upload right now, a live chart, top devices and destinations, today's totals, and a banner when the router stops exporting or an alert fires. The sister guide on the DisplayOps site, Live MikroTik traffic monitor on a wall display, puts it on a Raspberry Pi screen: which URL to use, the password prompt, and keeping the Pi on it. It starts from the collector this guide leaves you with.
Troubleshooting
- The container keeps restarting. Capture its logs from the device page (Logs, then Capture) or with
docker logson the host.bind: address already in usemeans another program holds the port: setHTTP_PORTto a free one and redeploy. - The deployment never turns healthy. Same cause, or the image has no build for the host's architecture. The deploy dialog blocks with "Architecture validation failed" when the release lacks the device's platform; the published image has both, a build of your own may not.
- Status says "No flow records received yet". The router's target does not point at this host, or UDP 2055 is blocked on the way.
/ip traffic-flow target printon the router. - Packets "rejected from other sources". The router exports from an address other than
ROUTER_IP. Setsrc-addresson the target to the router's LAN address. - Router API "invalid user name or password". The secret is wrong, or the
flowmonuser'saddressrestriction does not match the host. Set the secret again and deploy the release again; devices keep the old value until their next deployment. - Router API "TLS handshake failed".
api-sslhas no certificate. Step 1.3. - A changed variable has no effect. Application defaults are frozen into each release: cut a new one. Fleet and device variables apply as soon as they are saved.
- History is gone after a redeploy. The application was deleted and recreated, which gave the volume a new name. See the callout above.
- The installer refuses to run. Docker from snap, rootless Docker or Podman. Install Docker Engine from Docker's repository; the installer prints the steps and the supported devices page lists them.
Security checklist
- The router account has the
readandapipolicies only and works only from the host's address. api-sslhas a certificate and the collector pins it on first use. Without TLS the password crosses the LAN in the clear.ROUTER_PASSWORDis a secret, never a default: encrypted at rest, never shown again, delivered to the agent only with a release.- If anyone other than you can reach the host, add
NFP_AUTH_PASSWORD: ${WEB_PASSWORD}to the compose text and set it as another secret. Put a reverse proxy with TLS in front if the interface leaves the LAN. - The agent opens no inbound ports, and the collector's two ports are on your LAN only. Nothing in this guide opens anything to the internet.
- The API key has admin rights. Keep it in a secret store and create a separate key per system that uses it.
MikroTik NetFlow collector: questions we get
Do I need Simple Docker Ops to run the collector?
No. The repository's compose file and a .env file run it on any Linux machine with Docker, which is what step 3 does. Simple Docker Ops takes over what happens after the first "up": the router password as an encrypted secret instead of a file on the host, releases pinned to image digests, a health gate on every deployment, and upgrades and rollback from the portal or the API without SSH.
Why host networking?
The router sends flow records to the host's address on UDP 2055, and the collector only accepts packets from the exporters it knows. With host networking the packets arrive with the router's real source address and the web interface listens on HTTP_PORT directly, so there is no port mapping to maintain and nothing to get wrong about UDP through Docker's NAT. The agent runs the compose file as written; it does not rewrite or restrict it.
Can one application serve several routers?
Yes. The application's defaults are frozen into each release, and fleet variables and device variables override them: the same release, a different ROUTER_IP per device, and a ROUTER_PASSWORD secret scoped to each device. Saving fleet or device variables re-pushes the current release straight away.
What happens if the host is offline during a deployment?
The release waits for it. When the host reconnects the agent reconciles, reports health, and the deployment accounts for it. The agent treats each release as a transaction and reconciles again after a power loss, so nothing is half-applied.
What does it cost?
The collector is one device. The Free plan covers five devices and one gigabyte of managed registry, with no card and no time limit. Fleets, staged rollouts and encrypted secrets are included on every plan.
Where does the traffic data go?
Nowhere. Everything stays in one SQLite file in the netflow-data volume on the host. The collector makes no outbound connections except to the router and to a webhook you configure yourself. Simple Docker Ops sees the container's health, its captured logs and its configuration, never the traffic.
Which versions does this apply to?
RouterOS 7 (the collector was developed against 7.20), mikrotik-home-netflow-plus 1.1.0, Docker Engine 24 or newer and Compose v2 or newer. The collector is Apache 2.0 licensed and independent of both MikroTik and Simple Docker Ops.
Related
Deploy the collector once and forget about SSH.
Five devices and one gigabyte of registry on the Free plan. Paste the compose file, cut a release, deploy.
curl -fsSL https://simpledockerops.com/install | sudo sh