English · Русский
Central management for a fleet of MikroTik routers over the RouterOS API: monitoring, bulk upgrades, scheduled backups, syslog, WireGuard. Self-hosted, no cloud, MIT.
Runs on any server with Python, as a single process. No Redis, no Celery, no Node.js, no frontend build step. The panel itself needs no internet access: nothing is loaded from third-party sites.
The interface comes in English and Russian, switchable from the menu.
The code was written by Claude, an AI model by Anthropic. See How this project came about.
The prompt and the project context behind it: docs/prompt-mikrotik-panel.en.md.
Site: tikpilot.ru
The screenshots are taken from a live fleet with screenshot mode on: site names, addresses, client names and accounts are replaced with made-up ones by the panel itself. Addresses come from the ranges reserved for documentation.
Device list. Name, address, group, RouterOS version, uptime, status, time of the last check. Filters, search, sorting by any column. Add devices by hand or load a CSV file.
Bulk operations. Pick devices or a whole group and run:
- a status check;
- a reboot;
- a script that already sits on the device;
- pasted script source;
- a script upload without running it;
- any RouterOS API command;
- a command in console syntax over SSH, exactly as in Winbox, with wrapped lines joined back and the current menu remembered;
- the same with a safety net: the router rolls back to its previous configuration unless the change is confirmed;
- a backup (binary and export) downloaded to the server;
- setting the identity;
- a RouterOS upgrade;
- a speed test to another site in the fleet.
Everything runs in the background. You see the progress and a separate result for each device. A job can be cancelled, or deferred to a chosen time such as 02:00.
Needs attention. The first block on the dashboard: offline and flapping sites, packet loss, stale backups, syslog gone quiet, armed rollbacks, failed jobs, CPU, memory and disk pressure, open services and available updates - one list ordered by severity. When everything is calm, the block is not there.
Monitoring. The panel checks availability once a minute and keeps a history of outages with downtime totals. It shows a fleet map, uptime percentage over a day, a week or a month, and a separate list of sites that keep flapping.
Short drops are told apart from real outages by MONITOR_MIN_OUTAGE: they are
marked as flaps, kept out of the downtime figure and out of the feed, but still
visible as a separate count.
Link latency. The devices ping the configured targets and their own gateway themselves. The graphs show a link degrading before the site actually drops.
Separately, the panel pings on its own, but only the sites that did not answer over the API. The status answers the question "is the site working": a router that answers ping stays online even when the panel cannot reach its API. An unreachable API is a separate mark, "no API" in the list and a line on the card: the site is up, but commands do not reach it - the link, the tunnel or a service on the router. Offline means both are silent.
That is the right split: driving to a site and fixing a link are different
jobs, and downtime in a report should be about the site, not about our API.
Switch it off with ICMP_CHECK_ENABLED=0 and the API decides everything again.
Clients. What is connected at the sites: name, vendor, MAC, address, port, VLAN and when it was last seen. Built from DHCP leases, ARP and the bridge host table, so it also shows boxes with hand-set addresses and which port the cable is in. You can see whether a client is wired or wireless, and for wireless ones also the network and the signal level. A comment set on the router itself is shown as the name.
Backups. Files are stored on the server, searchable and downloadable.
Console. A tab showing what the panel is doing right now: checks, jobs, device errors. Lines live in memory, and passwords never reach them.
Device inventory. The device card shows ports as tiles coloured by link speed and marked where PoE is on, bridges and VLANs, neighbours from the discovery table, the router's enabled services, and sensor readings. A neighbour that also lives in the panel becomes a link to its own card.
Services deserve a separate mention: telnet, ftp, www without ssl, or api with no address restriction are flagged as risky. One forgotten site with telnet open is a hole, and you cannot find it by eye across fifty boxes.
Collected during the full poll, in the same session, and rendered from the database: the card opens instantly and still works when the site is down.
The WebFig button comes from the same data: if www or www-ssl is
enabled on the router, the card header links to the router's own web
interface, on the port recorded in the inventory and over https when that is
configured. No web service, no button. The link opens from your machine
rather than from the panel's server, so it only gets through from wherever
the site is reachable anyway.
Device logs. The panel receives syslog over UDP and TCP itself and keeps the lines next to everything else: a line is tied to its site right away, and you can jump from it to that device. Filters by text, site, severity and topic, a live feed that loads older lines as you scroll, and a text export.
Rules decide what happens to a line: highlight it in one of nine colours, hide it (still in the database, brought back by a checkbox), or do not store it at all. The second is for what irritates the eye, the third for what is not worth keeping. The panel's own API logins come with a ready made hide rule, switched off.
On a phone. The panel can be added to the home screen and opens like an app: no address bar, its own icon, a bottom tab bar within thumb reach and padding around the notch. Nothing to install, it is still a web page: Safari, Share, Add to Home Screen.
Audit log. Who did what and when.
Security. Login and password to get in. Administrator passwords are hashed (bcrypt), device passwords are encrypted (Fernet). After a few failed login attempts the address gets a pause that grows with every excess.
The numbers are measured on a live install with 50 devices, not estimated.
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 1 core | 2 cores |
| RAM | 512 MB | 1 GB |
| Disk | 2 GB | 10 GB and up if you keep backups |
| Python | 3.10 | 3.12 |
| OS | anything with systemd | Ubuntu 22.04 or 24.04 LTS |
The panel uses 57 MiB of memory at startup and 62 MiB with fifty devices. The install including the virtualenv takes 72 MiB. The database grows by about 1.6 MiB per day and then stops: old rows are pruned automatically.
Backups take the most space. A hAP ac lite copy is 80 to 150 KiB, a text export 15 to 60 KiB. For 49 sites that is roughly 67 MiB for seven daily copies.
A VM with one core and one gigabyte of RAM is plenty. The bottleneck is not the server, it is the links to the sites.
git clone https://github.com/maximdr86/tikpilot.git && cd tikpilot
cp .env.example .env
docker compose up -d --buildThe panel opens at http://your-server:8080. Login admin, password admin,
change it right after the first login.
Settings live in .env next to docker-compose.yml, on the host, not inside
the container. The image does not contain .env at all: the values are passed
in as environment variables at start. Edit the file and apply:
nano .env
docker compose up -dNo rebuild is needed for that, --build is only for code changes. To check what
actually arrived: docker compose exec tikpilot env | sort.
The published port is the left side of ports, for example "6060:8080". There
is no reason to change the inner one.
This is the way to go on a production server. Copy the project folder and run:
cd ~/tikpilot
sudo bash install-ubuntu.shThe script installs the dependencies, creates a system user, deploys the panel
to /opt/tikpilot, sets up autostart and prints the address and the password.
The same script works as an updater. Replace the files, run it again: the
database, the keys and .env stay where they are.
Managing the service:
sudo systemctl status tikpilot
sudo systemctl restart tikpilot
sudo journalctl -u tikpilot -fThe installer, restore-data.sh and run.sh speak English or Russian
depending on the server locale. Force one with TIKPILOT_LANG=en or
TIKPILOT_LANG=ru:
sudo TIKPILOT_LANG=ru bash install-ubuntu.shgit clone https://github.com/maximdr86/tikpilot.git && cd tikpilot
cp .env.example .env
./run.shOn Windows run run.bat. The script creates the environment and installs the
dependencies itself.
Python 3.10 or newer is required. Tested on 3.10 and on 3.14.
Where this actually runs. The panel is plain Python and will start almost anywhere, but it is meant for Linux: the installer writes a systemd service, and the tests run on Ubuntu against Python 3.10, 3.12 and 3.13. Docker is the same Linux inside, so it is the right answer on Windows and macOS too.
Running run.bat or run.sh directly on the machine is for having a look at
the panel rather than keeping a fleet on it: no service, no autostart, close
the window and the panel is gone. The ICMP check does work on all three
systems, the command is built for whichever one it is on.
requirements.txtuses lower bounds rather than pinned versions, on purpose. Prebuilt packages for a brand new Python release may not exist yet, and then pip tries to compile them from source, which needs Rust and a C++ toolchain. With floating versions it always finds a ready-made wheel.
Each device needs the API enabled and a dedicated user. Run this on the router:
# 1. Enable the API
/ip service set api disabled=no port=8728
# optionally restrict it to the server address:
/ip service set api address=192.0.2.10/32
# 2. FTP is only needed to download backups
/ip service set ftp disabled=no address=192.0.2.10/32
# 3. A group with minimal rights
/user group add name=tikpilot policy=api,read,write,test,ftp,reboot,policy
# 4. The user
/user add name=tikpilot group=tikpilot password="StrongPassword" address=192.0.2.10/32What each policy is for:
| Policy | Why |
|---|---|
api |
required, nothing works without it |
read |
status, version, uptime |
write |
configuration changes, creating scripts |
test |
running scripts |
ftp |
downloading backups |
reboot |
mass reboot |
policy |
needed for scripts that carry policies |
Do not grant sensitive unless you need an export containing passwords.
If you do not need reboots and backups, api,read,test is enough.
The password must use Latin letters and digits. RouterOS does not accept anything else over the API, and the panel says so up front.
If you want an encrypted channel, enable api-ssl on the router and tick
"use API-SSL" with port 8729 on the device page.
The easiest way to load a whole fleet: Devices → Import CSV.
name;host;username;password;group;comment
core-01;10.0.0.1;tikpilot;Secret123;Core;backbone
acc-01;10.0.1.10;tikpilot;Secret123;Access;entrance 1Recognised columns: name, host, username, password, api_port, ftp_port, ssh_port, group, comment. The delimiter (comma or semicolon) is detected
automatically. Groups that do not exist yet are created for you.
The devices.csv file in the project is a template with made-up data.
Every account has its own set of capabilities and its own scope. The model follows MeshCentral: not roles, but checkboxes.
Capabilities are granted one by one: editing devices, managing groups, viewing and downloading backups, the audit log, the settings, user management. Every bulk action is a separate right of its own, so an operator can be allowed to take backups and check status while reboots and RouterOS upgrades stay out of reach. A newly added action appears in the list automatically.
Scope limits what the account sees to chosen groups and individual devices. Everything outside is invisible: the device list, the dashboard, the fleet map, the backups, even other people's job results. "Check all" means all of their devices.
Roles survive as quick presets. Viewer, Operator and Administrator simply tick a set of boxes, and you edit from there.
A new account starts with no rights at all. The opposite would be a nasty surprise: an administrator adds someone "just to look" and that someone reboots the fleet. You also cannot change your own rights, or one wrong click would lock you out and the fix would be editing the database on the server.
Checks live on the server, not in the interface. A hidden button is a convenience: a URL can be typed by hand and a request sent with curl.
Every other account in the list has a «Password» button: set a new one without asking for the old. You cannot do this to yourself, there is a separate form for that and it asks for the current password.
Changing a password, yours or someone else's, ends all of that person's existing sessions: the session generation number moves and cookies issued earlier stop matching. A password that leaves someone else's tab working protects against nothing.
Instead of inventing a password for someone and sending it in a chat, issue an invitation under Settings. The person follows the link, picks a username and a password of their own, and lands inside; you grant the rights afterwards.
The link works once and expires, by default after two days. The sign up page is
an ordinary panel page, so it only opens from a trusted network
(ADMIN_NETWORKS): a link that goes astray is useless to an outsider. An
existing username cannot be taken over, and the token never reaches the audit
log, which keeps only the note saying who the invitation was for.
The address in the link is the one you are viewing the panel on. If admins come
in on one address while you use another, set the right one in PANEL_BASE_URL.
Do not confuse it with PUBLIC_BASE_URL: that one faces outward for the status
page, and the sign up page will not open from outside anyway.
Each group can have a link like /status/<token> that opens without signing
in. Hand it to contractors or the on-call shift: they see which sites are up,
which are down, and since when.
Alongside the current state it shows downtime over the last 24 hours, both per site and as a total for the group. "How was it today" gets asked more often than "how is it right now", and the figure is computed by the same code as in the panel, so the contractor and the administrator look at the same number.
The page picks its language from the browser, and a link in the corner switches it. The panel deliberately ignores the browser language; here it is the other way round, because the link is opened by an outsider.
The page serves exactly that and nothing more. Deliberately absent: device addresses (a list of internal IPs is a map of your network), RouterOS versions (useful mainly to whoever plans to exploit them), error texts (they describe your internals), and any other group.
The token is random and 32 characters long. The link is created and revoked with a button under Groups, and revoking takes effect immediately. Turning it back on always issues a new token.
Logs → Public links shows who opened a status page: address, device and browser, when the session started and when it was last active. A block at the top lists the pages that are open right now: the page refreshes itself once a minute, and that trail shows live tabs.
A row is a session, not a request, so a tab left open all day stays one row with a refresh counter. A link sent to a messenger is opened by the messenger first to build a preview; those visits are marked as robots and hidden from the list of people.
Visits with a token that does not exist are behind their own filter. One is a typo, ten in a row from one address is someone guessing: revoke the link and issue a new one. The wrong token itself is not stored, only its last six characters.
This is information about people, so visits.view is a separate right that is
not granted by default, and retention has its own setting:
PUBLIC_VIEW_DAYS=90
The address in the link comes from PUBLIC_BASE_URL. Without it the server
only knows the address the request arrived on, and since the panel is opened
from the inside, the link would carry a local 10.x:8080:
PUBLIC_BASE_URL=http://vpn.example.com:6060
The group list shows when each link was last opened and how many visits it had over the last day and week. Visits count people, not requests: the page refreshes itself every minute, so a tab left open counts once. A sudden rise means the address travelled further than you intended.
The page is closed to search engines and to caching. An unknown token and a revoked one answer identically, so guessing tells the visitor nothing.
The operator on the public page. Off by default: the page is meant to be "a site name and its state". If it helps the contractor to know whose link a site runs on, and it usually does since they will call the ISP themselves, tick "show the operator" next to the link. The tick belongs to one group.
Only the operator name is shown. The details, the public address, the radio technology and the signal level, stay inside the panel: that is information about the network rather than an answer to "whose link is this".
The WireGuard section sets up router-to-router links in a hub-and-spoke layout: one router is the hub, the other sites connect to it.
Creating a link adds a peer and routes to the far side's networks on the hub,
and hands you a ready .rsc for the spoke router. If the spoke is already in
the device list, the script can be applied to it with one button. For non-MikroTik
peers a plain wg-quick config is offered too.
Everything created is tagged with a wgpanel:<link> comment, so deleting a link
touches only its own peer and routes; the rest of the router configuration is
left alone.
Hub settings are read from the router itself: the interface, the tunnel address and the LAN subnets are filled in for you, so all that is left is to cross out what does not belong and save.
It all runs over the same device API the panel already uses to manage the fleet. No REST service, no certificate, no extra router user. The spoke private key is shown once and stored nowhere.
The config can be copied to the clipboard or downloaded as a file: the desktop
WireGuard app takes it by import, and to a phone it is easier to send as a
message. The panel shapes the filename into something the client will accept,
because the client takes the tunnel name from it and only allows latin letters,
digits and _=+.-, up to fifteen characters.
For a phone or a laptop, a QR code is shown next to the wg-quick config. It is
drawn on the server, so no internet access is needed for it. An existing link
has no code: the private key is gone by then, and without it there is nothing to
put on the phone.
One thing worth knowing about routes: in RouterOS a peer's allowed-address
decides which spoke a packet is encrypted for, but writes nothing into the
routing table. Networks behind the tunnel therefore need ordinary routes, which
the panel creates for you.
Their gateway is the spoke tunnel address, 10.8.0.43 for example, not the
interface name. With a single peer there is no difference; with several, a route
through the interface does not tell the router which one to hand the packet to.
When a site goes down the first question is not "what is wrong with the router" but "whose link is it". The device list has an "Operator" column, and search covers it too.
The panel finds it on its own, in this order:
- from the modem.
/interface/lte/monitorreports the network code, and the name comes from the code:25001is always MTS, while the string baked into the firmware differs from model to model. The technology and signal level are shown under the name; - from the address registry. Where there is no modem, the panel asks RDAP
about the public address of the site. This is the only place in the whole
panel that reaches out to the internet, so it is off by default:
OPERATOR_LOOKUP=1. Private and CGNAT addresses are never looked up: behind a carrier NAT the registry knows the owner of the NAT, not of the link; - by hand. The "Operator" field on the device card. What you type there wins and is never overwritten by a poll; a contract number fits nicely there too.
Rechecked once a day: an operator changes approximately never.
GET /healthz answers {"status": "ok"} with code 200 while the panel can
read its database, and {"status": "error"} with 503 when it cannot. No
login required and no trusted-network check: the caller is usually not a
person but the installer, systemd, a container monitor or an external
watchdog, and they arrive from an address that is not in the trusted list.
The answer is deliberately thin. The page is open to everyone, so it says nothing about the version, the number of devices or the uptime.
The first block on the dashboard. It answers the question "what should I deal with", not "how are things": one list sorted by severity instead of a walk through six sections.
What feeds it:
- availability - sites that are offlin

















0 comments
log in to comment.