Working Tailscale container on hEX S (EN7562CT, arm32v5) — 54 Mbit/s exit node

Every thread I could find says arm32v5 containers are a dead end for Tailscale. They are not. Here is a complete working recipe, tested today on hEX S E60iUGS / RouterOS 7.20.8 long-term (firmware NOT upgraded).

Result up front: 54 Mbit/s through the exit node, ~8 MB RAM idle / 32 MB under load, survives a power cut unattended (back online 30 s after power returns).

Three blockers, and how to get past each

1. There is no official Tailscale build for ARMv5

Docker images are published only for arm/v7 and arm64, and issue #676 was closed without armel support. But Go cross-compiles it fine, you just have to build it yourself:

docker run --rm -v "$PWD":/out golang:1.26 sh -c \
  'CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 go install \
   tailscale.com/cmd/tailscaled@latest tailscale.com/cmd/tailscale@latest && cp /go/bin/linux_arm/* /out/'

Go 1.26.5+ is required, Tailscale 1.102 refuses to build on 1.24. Verify with file tailscaled: you want ELF 32-bit LSB executable, ARM, EABI5, statically linked.

2. docker buildx and docker save produce an OCI layout RouterOS cannot import

This was the real time sink. The import fails with:

*** error getting layer file
import error: failed to load next entry

RouterOS needs the legacy docker-archive format (<hash>/layer.tar + manifest.json), while modern Docker exports blobs/sha256/... for both buildx -o type=docker and docker save. Convert with skopeo:

docker buildx build --platform linux/arm/v5 -t ts-armv5:1 -o type=docker,dest=oci.tar .
skopeo copy oci-archive:oci.tar docker-archive:ts-legacy.tar:ts-armv5:1

3. TUN mode needs a very recent RouterOS, skip it

--tun=userspace-networking (netstack) works and needs no firmware upgrade. Given the documented bricking reports on this exact model after RouterOS/RouterBOOT upgrades, not touching firmware is a feature, not a compromise. Speed is still 54 Mbit/s.

Dockerfile

FROM --platform=linux/arm/v5 busybox:stable
COPY tailscaled /usr/sbin/tailscaled
COPY tailscale  /usr/bin/tailscale
COPY start.sh   /start.sh
ENTRYPOINT ["/bin/sh", "/start.sh"]

Do not use RUN, set the exec bits on the host before building, otherwise buildx wants an armv5 emulator you probably do not have.

start.sh

#!/bin/sh
mkdir -p /state /var/run
echo "nameserver 1.1.1.1" > /etc/resolv.conf
while true; do
  /usr/sbin/tailscaled --state=/state/tailscaled.state \
    --socket=/var/run/tailscale.sock --tun=userspace-networking &
  TPID=$!
  sleep 10
  /usr/bin/tailscale --socket=/var/run/tailscale.sock up \
    --hostname=my-hexs --advertise-exit-node --accept-dns=false
  wait $TPID
  sleep 5
done

Without TS_AUTHKEY the auth URL simply shows up in /log print where topics~"container", open it in a browser to authorise the node.

RouterOS side

/interface veth add name=veth-ts address=172.30.0.2/24 gateway=172.30.0.1
/ip address add address=172.30.0.1/24 interface=veth-ts
/ip firewall nat add chain=srcnat action=masquerade src-address=172.30.0.0/24
/container add file=usb1-part1/ts-legacy.tar interface=veth-ts root-dir=ts-root \
  logging=yes start-on-boot=yes hostname=my-hexs
/container start [find]

The tar can sit on a FAT32 USB stick, only root-dir has to be on internal storage. The unpacked container takes ~72 MB there.

Measurements

All numbers are real transfers (25 MB download), not calculations:

  • exit node through the container: 54 Mbit/s
  • same link, no tunnel: 435 Mbit/s
  • kernel WireGuard on the same router: 28 Mbit/s (capped by the 32 Mbit/s uplink behind it)
  • CPU 5% idle after boot, RAM 8 MB idle and 32 MB under load
  • power-cut test: router pings 9 s after power returns, node back in the tailnet and advertising the exit node at 30 s

Softfloat turned out to matter far less than expected, ChaCha20-Poly1305 is integer arithmetic so the missing FPU costs little.

For contrast, on the same network an Android TV box (Amlogic S905Y4) running the official Tailscale app as an exit node gave 0.8 Mbit/s, this router is 67x faster.

Happy to answer questions if anyone wants to reproduce it on other EN7562CT devices.

First of all, thank you for sharing it on the forum.
I'll try your suggestion.

I was thinking of a different solution, but I'm not sure if it's feasible.

I already use a Debian container (Debian stable-slim with hAP ax S) for other services and was thinking of installing Tailscale on it soon as well.

I'll let you know if I succeed...

Reading this, I basically did the same thing a few weeks ago but for the hEX refresh.

https://codeberg.org/kitzin/tailscale-hex-refresh

Quoting your README.md, @kitzin

But we still compile for ARMv7 with software floating point it seems to work ¯\_(ツ)_/¯.

That seems like a risky path to me, depending on behavior that simply happens to work. The OP's solution of cross-compiling specifically for ARMv5 is better.

Incidentally, if you do change this to match the OP's scheme, note that using ENV to pass GOOS and friends is also an over-reach. ENV is meant to keep those values in the environment for the running process, not for passing ephemeral settings to RUN steps. Passing them ahead of the command as the OP does is better.

I've updated the ARMv5 section of my article on container.npk limitations to cover this possibility. It's a solid option, when available.