A lightweight Docker container implementation of Tailscale for MikroTik RouterOS devices, specifically tested on the hAP ax² model.
This project provides a lightweight Docker container (targeting under 30MB) for running Tailscale on MikroTik RouterOS devices. It enables secure, private network connectivity through Tailscale's mesh VPN technology.
- Device: MikroTik hAP ax² (RouterBOARD)
- Architecture: ARM64
- RouterOS Version: 7.14.2 or later (stable)
- Memory: 928.0 MiB total (minimum)
- Storage: 128.0 MiB total (minimum)
- A MikroTik hAP ax² router with RouterOS 7.14.2 or later
- A Tailscale account and auth key
- Basic understanding of MikroTik RouterOS configuration
- SSH access to your MikroTik router
First, set up the required network components on your MikroTik router:
# Create virtual interface
/interface/veth/add name=veth-tailscale address=192.168.98.2/24 gateway=192.168.98.1
# Configure bridge
/interface/bridge/add name=dockers
/ip/address/add address=192.168.98.1/24 interface=dockers
/interface/bridge/port/add bridge=dockers interface=veth-tailscale
# Add route for Tailscale network
/ip/route/add dst-address=100.64.0.0/10 gateway=192.168.98.2
Set up the required storage:
# Create a RAM disk (tmpfs). This is ONLY a staging area for the image .tar.
# It is wiped on reboot, which is fine: the .tar is only read during import.
/disk add type=tmpfs tmpfs-max-size=200M
# Create the container root dir and the persistent state dir.
# Both live on the router's internal flash, so they SURVIVE reboots.
/file/mkdir containers/tailscale
/file/mkdir containers/tailscale-state
Persistence note: the hAP ax² has no NVMe/USB by default — its only persistent storage is the internal flash. Earlier versions of this guide referenced
/nvme1/...; on a stock hAP ax² that is just a folder on flash and is not needed. Keep the containerroot-dirand the state mount on flash (as above) and the node stays authenticated across reboots. Do not pointroot-diror the state mount at the tmpfs disk — it is cleared on reboot.
Define the persistent state mount (the /container/add below references it):
# Persist Tailscale's state (node key, etc.) on flash so the node stays
# authenticated across reboots and container re-creation.
/container/mounts/add name=tailscale_state src=containers/tailscale-state dst=/var/lib/tailscale
Why one mount, not two? A common broken pattern defines two mounts that both target
/var/lib/tailscale(e.g.src=containers/tailscaleandsrc=containers/tailscale/state). They conflict, and the first one mounts the container'sroot-dirinto itself. Use the single mount above instead.
Configure the required environment variables:
/container/envs/add list=tailscale key=AUTH_KEY value=your-auth-key-here
/container/envs/add list=tailscale key=ADVERTISE_ROUTES value=192.168.88.0/24
/container/envs/add list=tailscale key=CONTAINER_GATEWAY value=192.168.98.1
/container/envs/add list=tailscale key=TAILSCALE_ARGS value="--accept-routes --advertise-exit-node"
list=vsname=: current RouterOS (verified on 7.20) expectslist=for the env-list name; older releases usedname=. If one returnsexpected end of command, use the other.Use
AUTH_KEY, notTS_AUTH_KEY: the entrypoint (tailscale.sh) readsAUTH_KEY. Setting onlyTS_AUTH_KEYleaves the key empty, and the container falls back to printing an interactivehttps://login.tailscale.com/a/...URL in the logs instead of authenticating. Use a reusable key so restarts work.
- Build the ARM64 container image:
./build-arm64.sh- Transfer the container image to the router:
scp tailscale-arm64.tar admin@192.168.88.1:tmp1/- Add the container to RouterOS (enter as a single line — RouterOS does not accept the multi-line form):
/container/add file=tmp1/tailscale-arm64.tar interface=veth-tailscale envlist=tailscale root-dir=containers/tailscale mounts=tailscale_state start-on-boot=yes hostname=mikrotik-tailscale dns=8.8.4.4,8.8.8.8 logging=yes
The container extracts asynchronously. Watch /container/print until it shows
stopped (not extracting), then start it and follow the logs:
/container/start 0
/log/print where topics~"container"
Because this node advertises subnet routes and an exit node, you must approve them once in the Tailscale admin console:
- Open https://login.tailscale.com/admin/machines.
- Find
mikrotik-tailscale, and under its route settings approve the subnet route (192.168.88.0/24) and enable it as an exit node. - (Recommended) Disable key expiry for the node so it stays connected across reboots without re-authentication.
To make Tailscale survive reboots without exhausting flash storage:
- Ensure the image tar is stored in persistent storage at
containers/tailscale/tailscale-arm64.taron the router. - Copy the boot script to the same directory:
scp tailscale-boot.rsc admin@<ROUTER_IP>:containers/tailscale/
scp tailscale-arm64.tar admin@<ROUTER_IP>:containers/tailscale/- Configure the container subsystem to use RAM for layers (prevents "No space left" and reduces flash wear):
/container/config/set layer-dir=tmpfs tmpdir=tmpfs/pull ram-high=200M
- Create a startup scheduler that imports the boot script on every boot:
/system/scheduler/add name=start-ts on-event="/import file-name=containers/tailscale/tailscale-boot.rsc" start-time=startup
What the boot script does on boot:
- Ensures the
tmpfslayer and pull dirs are set - Ensures
containers/tailscale/stateexists and is mounted astailscale_stateto/var/lib/tailscale - Re-adds the container from
containers/tailscale/tailscale-arm64.tarif missing - Starts the container
Recommended RouterOS-specific args:
- Add
--netfilter-mode=offtoTAILSCALE_ARGSto avoid iptables/ip6tables errors on MikroTik kernels and rely on RouterOS firewalling instead. - After successful auth, remove one-time auth envs to avoid re-logins:
/container/envs/remove [find where name="tailscale" and key="AUTH_KEY"]
/container/envs/remove [find where name="tailscale" and key="TS_AUTH_KEY"]
Follow these steps to bring up the same setup on another MikroTik (RouterOS 7.14+), with minimal writes to flash.
Adjust IPs as needed. Example uses veth-tailscale at 172.17.0.2/24 with gateway 172.17.0.1 and a bridge dockers.
# Create veth and bridge (if not already present)
/interface/veth/add name=veth-tailscale address=172.17.0.2/24 gateway=172.17.0.1
/interface/bridge/add name=dockers
/interface/bridge/port/add bridge=dockers interface=veth-tailscale
# Route for Tailscale control plane & relays
/ip/route/add dst-address=100.64.0.0/10 gateway=172.17.0.2
# Ensure persistent directories exist
:if ([:len [/file find where name="containers"]] = 0) do={ /file add name="containers" type=directory }
:if ([:len [/file find where name="containers/tailscale"]] = 0) do={ /file add name="containers/tailscale" type=directory }
From your workstation (repo root):
scp tailscale-arm64.tar admin@<ROUTER_IP>:containers/tailscale/
scp tailscale-boot.rsc admin@<ROUTER_IP>:containers/tailscale/# Required keys for ongoing operation
/container/envs/add name=tailscale key=ADVERTISE_ROUTES value=192.168.92.0/22
/container/envs/add name=tailscale key=CONTAINER_GATEWAY value=172.17.0.1
/container/envs/add name=tailscale key=TAILSCALE_ARGS value="--accept-routes --advertise-exit-node --netfilter-mode=off"
# Optional: one-time auth for first bring-up only (remove after success)
# /container/envs/add name=tailscale key=AUTH_KEY value=tskey-xxxxx
/container/config/set layer-dir=tmpfs tmpdir=tmpfs/pull ram-high=200M
/system/scheduler/add name=start-ts on-event="/import file-name=containers/tailscale/tailscale-boot.rsc" start-time=startup
/import file-name=containers/tailscale/tailscale-boot.rsc
The import will ensure the state mount exists, (re)add the container from the tar if missing, and start it.
/container/print detail where hostname="mikrotik-tailscale"
/log/print where topics~"container" time>[:totime "now-5m"]
/ping address=100.100.100.100 count=2
# After successful auth, remove one-time keys if you used them:
# /container/envs/remove [find where name="tailscale" and key="AUTH_KEY"]
# /container/envs/remove [find where name="tailscale" and key="TS_AUTH_KEY"]
/system/reboot
# After boot
/container/print detail where hostname="mikrotik-tailscale"
/ip/route/print where dst-address=100.64.0.0/10
/ping address=100.100.100.100 count=2
# Check virtual interfaces
/interface/veth/print
# Check bridge configuration
/interface/bridge/print
# Check bridge ports
/interface/bridge/port/print
# Check IP addresses
/ip/address/print
# List containers
/container/print
# Get detailed container information
/container/print detail
# Start container
/container/start [container_index]
# Remove container
/container/remove [container_index]
# Check container logs
/log/print
# Enable logging for a container
/container/set [container_index] logging=yes
# List environment variables
/container/envs/print
# Add environment variable (use list= on current RouterOS; older used name=)
/container/envs/add list=tailscale key=AUTH_KEY value=your-auth-key-here
# Remove environment variable
/container/envs/remove [find key=AUTH_KEY]
# Update environment variable
/container/envs/set [find key=CONTAINER_GATEWAY] value=192.168.98.1
# List files
/file/print
# List disks
/disk/print
# Check system resources
/system/resource/print
-
Architecture Mismatch
- Symptom: "Exec format error"
- Solution: Ensure you're using the ARM64 version of the container
-
Layer Extraction Issues
- Symptom:
*** error getting layer file/import error: failed to load next entry - Cause: modern Docker (BuildKit / containerd image store) writes
docker savearchives as OCI layout with gzip-compressed layers, which RouterOS cannot import. (The tar transferred fine — the format is the problem.) - Solution: build with the provided
./build-arm64.sh, which now repacks the image with uncompressed layers viapack-for-routeros.py. To fix a tar you already have:python3 pack-for-routeros.py old.tar tailscale-arm64.tar.
- Symptom:
-
Container starts but never authenticates (prints a login URL)
- Symptom: logs show
To authenticate, visit: https://login.tailscale.com/a/... - Cause: the auth key was not passed. The entrypoint reads
AUTH_KEY; setting onlyTS_AUTH_KEYleaves it empty. - Solution:
/container/envs/add list=tailscale key=AUTH_KEY value=<reusable-key>, then restart the container.
- Symptom: logs show
-
health(warnable=router): ... iptables (nf_tables): Could not fetch rule set generation id- This warning is expected on RouterOS — the container cannot program the host's netfilter. Routing/NAT is handled by RouterOS itself, so connectivity still works. No action needed.
-
Container Not Starting
- Check container logs:
/log/print where topics~"container" - Verify environment variables:
/container/envs/print - Ensure mount points exist and are accessible
- Check container logs:
The container root-dir (containers/tailscale) and the state mount
(containers/tailscale-state) live on internal flash, and the container has
start-on-boot=yes, so after a reboot it auto-starts and reconnects using the
persisted node key — no re-upload of the .tar and no re-auth required. Only the
import .tar on the tmpfs disk is lost on reboot, which does not affect runtime.
- Always use a secure Tailscale auth key
- Regularly update RouterOS to the latest stable version
- Monitor container logs for any suspicious activity
- Use appropriate firewall rules to restrict access
- Regularly check for RouterOS updates
- Monitor container logs for issues
- Verify Tailscale connectivity after router reboots
- Keep the container image updated
Contributions are welcome! Please feel free to submit a Pull Request.
This project is open source and available under the MIT License.
For issues and feature requests, please use the GitHub issue tracker.