In this post I’ll show you example of how to configure tailscale subnet router in a container on amikrotik router.
This is not a terraform tutorial. It will not show you how to set up terraform for mikrotik from scratch. (If there is interest I may write basic routeros terraform tutorial and routeros terraform firewall tutorial.)
Some context
Terraform provider for routeros is one of my best discoveries of 2024.
It’s configuration as code with real idempotency we so love.
Since learning it I’ve switched completely o managing my mikrotik devices with terraform.
It’s soo good I personally wouldn’t consider buying managed networking device without terraform provider.
https://registry.terraform.io/providers/terraform-routeros/routeros/latest/docs
I’ve recently started a project to rebuild core of my home network into something less homelab-y.
Time to ditch “HA everything” mindset and build smaller core of the network on less hardware and save cash on power bill.
One part of that project was testing out replacing my forbidden router linux VM on a HA proxmox cluster with a simple RB5009UPr+S+IN router. 4 arm64 cores, 1GB of RAM and POE for all my APs in one box, what’s not to love.
Right now my “downsized lab” lives on the separate, fake ISP vlan and consists of the rb5009 router and optiplex k8s nodes for different services. Due to bugs and firewall incompatibilities I cannot run tailscale so I decided to try running tailscale subnet router on my router.
- Not very straight up installation
Enable container support on the device (requires you to physically reset or power off the device)
/system/device-mode/update container=yes
You actually need to also install the container package on the device.
check for updated to make sure router know how to install the package (I already have it installed)
/system/package/update/check-for-updates
/system/package/print
Flags: X - DISABLED; A - AVAILABLE
Columns: NAME, VERSION, BUILD-TIME, SIZE
# NAME VERSION BUILD-TIME SIZE
0 routeros 7.20.8 2026-01-30 09:17:54 12.6MiB
1 container 7.20.8 2026-01-30 09:17:54 208.1KiB
2 XA calea 20.1KiB
3 XA dude 1224.1KiB
4 XA extra-nic 2384.1KiB
Tell the router to install the package
/system/package/enable container
Tell the router to apply changes and actually install the package
/system/package/apply-changes
After this second reboot you should have /container path in shell of your router.
- Container storage
Mikrotik routers don’t enough flash to store and run containers. You will need an usb flash drive.
I’ve purchased SanDisk Ultra Fit for it. It’s reall small and doesn’t stick out too much from the router.
Usb drive should show up as usb1. You can check it’s name via /disk/print
Format your usb drive
/disk format-drive usb1 file-system=ext4
Create some directories (routeros is not smart enough to create them for you)
Paths don’t start with /
Router os has real paths starting with / internally but you don’t have access to them. Passing paths / risks confusing routeros.
/file add type=directory name=usb1/containers/tmp
/file add type=directory name=usb1/containers/layers
/file add type=directory name=usb1/containers/images
/file add type=directory name=usb1/containers/tailscale/var
- Terraform config, Preparing the network
One of the minor pain points of containers on this platform is networking. You have to do everything yourself and by hand.
You will need bridge and veth for your container and some routers.
In this case this bridge is dedicated just to tailscale container.
Here is example of my network config for this container:
resource "routeros_interface_veth" "veth-container-tailscale" {
name = "veth-container-tailscale"
address = [
"10.x.xx.2/24",
"fdxx:x:x:x::2/64"
]
gateway = "10.x.x.1"
comment = "Virtual interface for tailscale container"
}
import {
to = routeros_interface_veth.veth-container-tailscale
id = "*X"
}
resource "routeros_interface_bridge" "bridge_container_tailscale" {
name = "bridge_container_tailscale"
}
import {
to = routeros_interface_bridge.bridge_container_tailscale
id = "*X"
}
resource "routeros_ip_address" "ip_address_bridge_container_tailscale" {
address = "10.x.x.1/24"
#comment = null
#disabled = false
interface = "bridge_container_tailscale"
}
import {
to = routeros_ip_address.ip_address_bridge_container_tailscale
id = "*X"
}
resource "routeros_ipv6_address" "ipv6_address_bridge_container_tailscale" {
address = "fdx:x:x:x::1/64"
#comment = null
#disabled = false
interface = "bridge_container_tailscale"
advertise = false
}
import {
to = routeros_ipv6_address.ipv6_address_bridge_container_tailscale
id = "*X"
}
resource "routeros_interface_bridge_port" "bridge_container_tailscale_veth_port" {
bridge = "bridge_container_tailscale"
interface = "veth-container-tailscale"
}
import {
to = routeros_interface_bridge_port.bridge_container_tailscale_veth_port
id = "*X"
}
resource "routeros_ip_route" "route_bridge_container_tailscale" {
dst_address = "100.64.0.0/10" # tailscale internal subnet
gateway = "10.x.x.2" # container IP
}
import {
to = routeros_ip_route.route_bridge_container_tailscale
id = "*X"
}
resource "routeros_ipv6_route" "routev6_bridge_container_tailscale" {
dst_address = "fd7a:115c:a1e0::/48" # tailscale internal subnet
gateway = "fdx:x:x:x::2" # container IPv6
}
import {
to = routeros_ipv6_route.routev6_bridge_container_tailscale
id = "*X"
}
I try to always write imports in my configs. In mikrotik you usually get the ID by running print show-ids
For example
/ip/address/print show-ids
Flags: D - DYNAMIC
Columns: ADDRESS, NETWORK, INTERFACE
* ADDRESS NETWORK INTERFACE
...
*A 10.x.x.1/24 10.x.x.0 bridge_container_tailscale
Id is *A and my import config should look like this:
import {
to = routeros_ip_address.ip_address_bridge_container_tailscale
id = "*A"
}
Now run terraform apply, confirm changes and you should have your network configured.
- Creating config for you container.
First, we have to setup enviroment variables for our container.
I’m configuring the container to:
- use my headscale server login server.
- act as an exit node
- accept routes
- authenticate from one time preauth key
- advertise some routes (subnet router)
- and save config between restarts (you don’t want to reauthenticate node every container restart)
resource "routeros_container_envs" "env_tailscale_ts_extra_args" {
name = "tailscale"
key = "TS_EXTRA_ARGS"
value = "--login-server=https://XXXX.XXXXX.XXXX --accept-routes --advertise-exit-node "
}
import {
to = routeros_container_envs.env_tailscale_ts_extra_args
id = "*X"
}
resource "routeros_container_envs" "env_tailscale_ts_auth_key" {
name = "tailscale"
key = "TS_AUTHKEY"
value = "XXXXXXXXXXXX"
}
import {
to = routeros_container_envs.env_tailscale_ts_auth_key
id = "*X"
}
resource "routeros_container_envs" "env_tailscale_ts_routes" {
name = "tailscale"
key = "TS_ROUTES"
value = "10.x.x.0/24,fdx:x:x:x::/64"
}
import {
to = routeros_container_envs.env_tailscale_ts_routes
id = "*X"
}
resource "routeros_container_envs" "env_tailscale_ts_state_dir" {
name = "tailscale"
key = "TS_STATE_DIR"
value = "/var/lib/tailscale"
}
import {
to = routeros_container_envs.env_tailscale_ts_state_dir
id = "*X"
}
Next we configure volume mount for our container
Sadly import for routeros_container_mounts is currently borked in 1.99.0, no import.
resource "routeros_container_mounts" "tailscale" {
name = "tailscale"
src = "usb1/containers/tailscale/var"
dst = "/var/lib/tailscale"
}
And finally we have to configure some default /container settings
Configure default registry and working directories on our usb storage.
resource "routeros_container_config" "config" {
registry_url = "https://docker.io"
tmpdir = "usb1/containers/tmp"
layer_dir = "usb1/containers/layers"
}
You can run terraform apply to apply changes.
- Actually starting the container???
Finally we have arrived at the important part. Something that takes just seconds with docker compose.
Our container config (some of the config values are take straight from other terraform objects
resource "routeros_container" "tailscale" {
remote_image = "tailscale/tailscale:v1.94.2"
interface = routeros_interface_veth.veth-container-tailscale.name
logging = true
root_dir = "usb1/containers/tailscale/root"
envlist = routeros_container_envs.env_tailscale_ts_extra_args.name
mounts = [ "tailscale" ]
check_certificate = true
running = true
start_on_boot = true
}
terraform apply will provide us with nice changelog:
Terraform used the selected providers to generate the following execution plan. Resource actions are
indicated with the following symbols:
+ create
Terraform will perform the following actions:
# routeros_container.tailscale will be created
+ resource "routeros_container" "tailscale" {
+ arch = (known after apply)
+ check_certificate = true
+ envlist = "tailscale"
+ id = (known after apply)
+ interface = "veth-container-tailscale"
+ logging = true
+ mounts = [
+ "tailscale",
]
+ name = (known after apply)
+ os = (known after apply)
+ remote_image = "tailscale/tailscale:v1.94.2"
+ root_dir = "usb1/containers/tailscale/root"
+ running = true
+ start_on_boot = true
+ status = (known after apply)
+ stop_signal = "15-SIGTERM"
+ tag = (known after apply)
+ workdir = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value:
After terraform creates the container it should be visible in routeros…
/container/print
Flags: S - STOPPED
Columns: NAME, ROOT-DIR, INTERFACE, MOUNTS, ENVLISTS, TAG
# NAME ROOT-DIR INTERFACE MOUNTS ENVLISTS TAG
;;; import error: check url failed: SSL: ssl: no trusted CA certificate found (6)
0 S tailscale:v1.94.2 usb1/containers/tailscale/root veth-container-tailscale tailscale tailscale docker.io/tailscale/tailscale:v1.94.2
Yea, In 2026 we don’t have enough CA installed to pull the image from dockerhub.
(Github registry also has the same issue.)
You don’t even get the error with hostname at fault.
Disapointment levels: immeasurable
But we can disable TLS check and just hope for the best…
Setting check_certificate = false in container settings does just that.
Routeros /container doesn’t handle well changes to container settings.
I found out that it’s best to manually remove container before recreating it
cli command /container/remove tailscale:v1.94.2
After running terraform apply again we endup with…
/container/print
Flags: S - STOPPED
Columns: NAME, ROOT-DIR, INTERFACE, MOUNTS, ENVLISTS, WORKDIR, TAG
# NAME ROOT-DIR INTERFACE MOUNTS ENVLISTS WORKDIR TAG
;;; import error: failed to extract layer: createDirectory /flash/rw/disk/usb1/containers/layers/3449061238c60ed2f8808f5cc4bf23112bf819eda6bda4635b01c30aec6c9eb9//usr/local/bin mkdir failed: No such file or directory
0 S tailscale:v1.94.2 usb1/containers/tailscale/root veth-container-tailscale tailscale tailscale / docker.io/tailscale/tailscale:v1.94.2
It just cannot start container pulled from the web.
I had to switch to manually downloaded container file.
Quick tutorial for podman:
Download arm64 container on your system
podman pull ghcr.io/tailscale/tailscale:v1.94.2 --arch arm64
get arm64 container digest from the manifest
podman manifest inspect ghcr.io/tailscale/tailscale:v1.94.2 | jq -r '.manifests[] | select(.platform.architecture == "arm64") | .digest'
Save arm64 container to a tar file using it’s correct sha256 from manifest (useful if you already have amd64 containers pulled on the same host)
podman save ghcr.io/tailscale/tailscale@sha256:3e3bebce61524ec3fc416ecafba5b378a4d26d1609d5af8d25c9f49d4bb1ff59 -o tailscale_v1.94.2.tar
Upload container to the router usb1 drive
scp tailscale_v1.94.2.tar rb5009upr-0:/usb1/containers/images/
Now have to delete old routeros container
/container/remove tailscale:v1.94.2
Change our container definition to:
resource "routeros_container" "tailscale" {
file = "usb1/containers/images/tailscale_v1.94.2.tar"
interface = routeros_interface_veth.veth-container-tailscale.name
logging = true
root_dir = "usb1/containers/tailscale/root"
envlist = routeros_container_envs.env_tailscale_ts_extra_args.name
mounts = [ "tailscale" ]
check_certificate = false
running = true
start_on_boot = true
}
Run tarraform apply
And manually start the container on our router /container/start tailscale:v1.94.2
If everything goes well you should see R in from of your container
/container> print
Flags: R - RUNNING
Columns: NAME, ROOT-DIR, INTERFACE, MOUNTS, ENVLISTS, WORKDIR, MEMORY-CURRENT
# NAME ROOT-DIR INTERFACE MOUNTS ENVLISTS WORKDIR MEMORY-CURRENT
0 R tailscale_v1.94.2 usb1/containers/tailscale/root veth-container-tailscale tailscale tailscale / 45.5MiB
and a moment of truth
tailscale ping rb5009upr-0
pong from rb5009upr-0 (100.64.0.X) via DERP(xxx) in 25ms
pong from rb5009upr-0 (100.64.0.X) via DERP(xxx) in 20ms
pong from rb5009upr-0 (100.64.0.X) via DERP(xxx) in 18ms
pong from rb5009upr-0 (100.64.0.X) via DERP(xxx) in 17ms
pong from rb5009upr-0 (100.64.0.X) via DERP(xxx) in 20ms
If you encounter some tailscale issues, you can check the container logs here: /container/log/print
You can enter container shell via /container/shell tailscale_v1.94.2
Or execute commands straight from routeros shell /container/shell tailscale_v1.94.2 cmd="tailscale status"
TS_STATE_DIR is really important, it keeps config of your node. Your preauth key usually works only once on headscale. Without persistent TS_STATE_DIR you will need a new preauthkey every container restart.
It took me few tries to get tailscale config working. When reconfiguring things it’s best to delete routeros container and recreate it. Terraform can sometimes do this but not always.
Sadly it also means, that your container ID changes, you may need to update import config a lot.
To summarize
I’ve sunk few hours yesterday setting this up. I got it working but I cannot recommend containers on routeros.
I would have been fine with tiresome network setup and environment variable handling.
But inability to pull containers from THE docker container registry in 2026 is shameful.
Uploading container files to router could be automated, but it’s significantly more difficult than just changing container tag and letting k8s or docker replace container with latest image. Barriers to upgrade make you lazier when performing to upgrades and less secure.
Everything in container package seems to circle around manually deleting and manually recreating containers. It’s so bad people have written 60 line scripts just to restart containers to newer version.
Lack of polish and general design of containers on routeros are in my opinion also a reason why mikrotik RDS2216-2XG-4S+4XS-2XQ will be a complete failure. (Even without considering current flash prices). Mikrotik has created a very interesting piece of hardware and shacked it to it’s bizarre software.

