vinnymarquez.dev/tutorials/exposing a service over tailscale
August 8, 20262 min readcheat sheet#homelab#networking#tailscale

Exposing a Service Over Tailscale

giving one container its own tailnet address, without a public port-forward and without exposing the whole LAN with a subnet router.

wanted a self-hosted service reachable by people who already use my tailnet for something else, without punching a hole in the firewall for it. tailscale makes this genuinely easy — the only real friction was one container-specific setup step that isn’t obvious the first time.

two patterns, pick one

pattern blast radius when to use it
per-container client just that one service default choice — exposes exactly what you mean to
subnet router the entire LAN/VLAN it advertises only if you actually want every device on that network reachable

per-container is the one i’d default to almost every time. a subnet router is more convenient if you’re exposing a lot of services at once, but it’s a bigger blast radius for what was, in my case, one service.

if it’s a container: TUN device first

the tailscale client needs /dev/net/tun, which an unprivileged container doesn’t have by default. i went looking for a toggle for this in my hypervisor’s UI and there isn’t one — it’s a conf-file edit, done from the host, not inside the container itself.

  1. shell into the node/host directly, not the container’s own console
  2. edit the container’s config file
  3. add:
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net dev/net none bind,create=dir

(10:200 is the standard Linux major:minor for /dev/net/tun — stable across kernel versions, not specific to any one hypervisor version.)

  1. restart the container — conf changes don’t apply to one that’s already running
  2. verify inside the container: ls -l /dev/net/tun should show a character device, not “no such file or directory”

don’t assume your hypervisor’s UI has a checkbox for this just because an old guide says so. check the actual live options page for your version first — mine didn’t have one, despite what i’d read.

LXC conf file with TUN passthrough lines

install and join

curl -fsSL https://tailscale.com/install.sh | sh
tailscale up

prints a login URL — open it, sign in with the same account your other tailnet devices use, approve the node.

running a self-hosted control server instead of tailscale.com’s default? add --login-server=https://your-server-url. otherwise plain tailscale up is correct — and don’t point --login-server at your tailnet’s own MagicDNS domain by mistake, that’s not a control-server URL, the command will just hang waiting on something that isn’t listening.

find it, share it, verify it

then check the boring stuff: the service is reachable from another device already on the tailnet, the original LAN URL still works locally (this only adds access, doesn’t remove it), and a device not on the tailnet can’t reach the new address.

one last thing — if the service’s LAN address is referenced anywhere by IP, pin it as a static reservation so it doesn’t drift. a floating DHCP lease that happens to be stable today isn’t guaranteed to stay that way, and “why did the bookmark stop working” is an annoying way to find that out.

▌ comments