An unprivileged LXC container on Proxmox cannot see /dev/net/tun, so tailscaled dies on its first start with CreateTUN("tailscale0") failed. You do not need a privileged container or a third-party script to fix that, because Proxmox has its own device passthrough for containers. One pct set line or one dialog in the web UI hands the TUN device over, which is the cleanest way to run Tailscale in a Proxmox LXC container that stays unprivileged.
This guide installs Tailscale in an unprivileged Debian 13 container, passes the TUN device both ways (pct set --dev0 and the GUI’s Device Passthrough), then turns the container into a subnet router and an exit node. It also covers the userspace fallback for containers whose config you cannot touch, and the errors we hit. Every command below was run in November 2026 on a Proxmox VE 9.2 node with Tailscale 1.104.4 inside Debian 13 containers.
Plan the container layout
We used four containers on one Proxmox node so that each claim in this guide could be proven from another machine on the tailnet. The point of the layout is the second bridge: vmbr1 has no IP on the host and only the router container has a leg on it, so the only way into 10.20.20.0/24 is the Tailscale subnet route.

Each container has one job in the lab:
- CT 200
ts-router: TUN viapct set, subnet router and exit node, 10.10.10.20 and 10.20.20.20 - CT 201
ts-client: TUN via the web UI, accepts routes, 10.10.10.21 - CT 202
web01: plain HTTP on 10.20.20.30, reachable only throughvmbr1 - CT 203
ts-userspace: no TUN device at all, userspace networking
The tailnet itself was coordinated by a self-hosted Headscale server on the lab host, so our tailscale up lines carry --login-server and route approvals run as headscale commands. On Tailscale’s hosted service you drop that flag, open the login URL it prints, and approve routes in the admin console instead. Nothing about the container side changes.
Create the unprivileged container
Skip this section if you already have a container. Otherwise refresh the template list on the Proxmox host and look for the current Debian 13 image, since the file name changes with each point release:
pveam update
pveam available --section system | grep debian-13
On test day the amd64 image was debian-13-standard_13.6-1.
system debian-13-standard_13.6-1_amd64.tar.zst amd64
system debian-13-standard_13.6-1_arm64.tar.zst arm64
Download it to the local storage, using the name your pveam available printed.
pveam download local debian-13-standard_13.6-1_amd64.tar.zst
Now create the container. --unprivileged 1 is the part that matters for this guide, and nesting=1 matches what the web UI’s Create CT wizard sets on its own. Our lab node keeps root disks on the local directory storage, so the rootfs reads local:4; on a node installed from the Proxmox ISO you’d normally write local-lvm:4 (ext4 or xfs install) or local-zfs:4 (ZFS install) instead. Change the bridge, address and gateway to match your network:
pct create 200 local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst \
--hostname ts-router --unprivileged 1 --features nesting=1 \
--cores 1 --memory 512 --rootfs local:4 \
--net0 name=eth0,bridge=vmbr0,ip=10.10.10.20/24,gw=10.10.10.1 \
--nameserver 1.1.1.1 --onboot 1 --start 1
Confirm the container is running and that it really is unprivileged. Notice that /dev/net/tun is missing inside it, even though the host has one:
pct config 200 | grep -E "unprivileged|features"
pct exec 200 -- ls -l /dev/net/tun
The config is right and the device is still absent, which is the whole problem in two lines. Proxmox does not hand host devices to an unprivileged container unless you list them, so you will fix this from the host, not from inside the container:
features: nesting=1
unprivileged: 1
ls: cannot access '/dev/net/tun': No such file or directory
Container 200 is the one we’ll use as the router for the rest of the guide, so substitute your own CTID wherever you see 200. The Debian and Ubuntu LXC guide covers template choices and storage in more depth.
Install Tailscale inside the container
Install the package first and add the device afterwards. That order shows you the real failure, which is worth recognising because it’s the same message you’ll get on any container that loses its device later. Open a shell in the container from the Proxmox host:
pct enter 200
You are root inside the container now, so no sudo is needed. Install curl, then add Tailscale’s signing key and its repository for Debian 13 (trixie). Use the repository rather than the one-line install script, because apt then keeps the package updated with the rest of the container.
apt update && apt install -y curl ca-certificates
curl -fsSL https://pkgs.tailscale.com/stable/debian/trixie.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null
curl -fsSL https://pkgs.tailscale.com/stable/debian/trixie.tailscale-keyring.list | tee /etc/apt/sources.list.d/tailscale.list
The second tee echoes the repository line it wrote. Check the codename in it, because a container built from an older Debian 12 template needs the bookworm files instead, and a wrong suite is the usual reason apt cannot find the package:
# Tailscale packages for debian trixie
deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/debian trixie main
Install the package and check the version.
apt update && apt install -y tailscale
tailscale version
The stable channel served 1.104.4 on test day. Every warning and health check shown later comes from this build, so if your version is much older, expect some of those messages to read differently or not appear at all:
1.104.4
tailscale commit: 9382131983ca16de1357091618fd903e35db3c4e
long version: 1.104.4-t938213198-gd38c96d6a
other commit: d38c96d6afce903d0a81e5f3e32d7048bc33b660
go version: go1.27.3 (tailscale/go 7ef7b40364)
The package enables tailscaled straight away, and it fails within a second. Its journal says exactly why:

Read the modprobe line carefully, because it sends people down the wrong path. Our Proxmox kernel (7.0.14-26-pve) has CONFIG_TUN=y, so TUN is built in and there is no module to load, which is why modprobe tun finds nothing. The host already has /dev/net/tun; the container simply is not allowed to see it. Loading modules or switching to a privileged container will not help, and you do not need either.
Leave the container shell with exit before the next section, since the device is added from the Proxmox host. There are two ways to do it and both write the same dev0 line to the container config.
Pass /dev/net/tun with pct set
The devN container option (documented in pct(1) as --dev[n] [[path=]<Path>] with optional mode, uid, gid and deny-write) replaces the raw lxc.cgroup2.devices.allow and lxc.mount.entry lines older guides paste into /etc/pve/lxc/<CTID>.conf. Proxmox validates it, shows it in the GUI and lets admins other than root restore the container with it intact, whereas a non-root restore skips raw lxc.* lines. On the host, run:
pct set 200 --dev0 /dev/net/tun
On Proxmox VE 9 you don’t have to stop the container first. Our container was running when we added the device and it appeared inside immediately, which matches the 9.0 change that lets devices be hotplugged into running containers. Check both the config and the container.
pct config 200 | grep dev0
pct exec 200 -- ls -l /dev/net/tun
Look for a character device with major 10, minor 200. Those are the numbers the host’s own /dev/net/tun carries, so a match means the container sees the host’s device node and not some other file at that path:
dev0: /dev/net/tun
crw-rw---- 1 root root 10, 200 Nov 2 16:27 /dev/net/tun
The hotplugged node came in as crw-rw----. After a full container reboot the same device showed as crw-rw-rw-, so do not be surprised if the mode differs between a live add and a fresh start; tailscaled runs as root and worked with both. By the time you add the device, systemd has given up on tailscaled after five quick failures, so clear that state and start it again:
pct exec 200 -- systemctl reset-failed tailscaled
pct exec 200 -- systemctl restart tailscaled
pct exec 200 -- systemctl status tailscaled --no-pager
This time the daemon stays up and reports Needs login, and a tailscale0 interface exists inside the container:

Tailscale’s own LXC page also suggests pct set <CTID> --features keyctl=1,nesting=1. Our container never got keyctl, as the features: nesting=1 line above shows, and tailscaled ran, joined, routed and survived reboots without it. Add it only if something else in the container asks for it.
Add the TUN device from the web UI
The web UI writes the same dev0 entry, and it’s the easier route when you’re already in the browser. We used it on CT 201 (ts-client), which had Tailscale installed and failing exactly like CT 200 did. Select the container in the resource tree, then:

- 1Resources
- 2Add
- 3Device Passthrough
- Open Resources in the container’s menu.
- Click Add above the resource list.
- Pick Device Passthrough from the menu.
The Add: Device dialog asks for a single value. Leave the Advanced box unticked unless you need a specific mode or owner:

- 1Device Path
- 2Add
- Type
/dev/net/tunin Device Path. - Click Add to save it.
The new row shows up in the container’s resources as Device (dev0). Like the CLI method, it was applied to the running container with no restart:

Back on the host, the config line is identical to what pct set wrote, and tailscaled comes up once you clear its failed state:
pct config 201 | grep dev0
pct exec 201 -- systemctl reset-failed tailscaled
pct exec 201 -- systemctl restart tailscaled
pct exec 201 -- systemctl is-active tailscaled
Both checks pass on CT 201. The GUI and the CLI write the same key, so you can switch a container between the two methods later without any cleanup:
dev0: /dev/net/tun
active
Pick whichever method fits how you manage the node. If you build containers from scripts or Ansible, pct set is the one to keep, because it is one idempotent line you can rerun. The web UI suits a one-off container, and it has the advantage that you see the device listed under Resources the next time you open the container, so nobody wonders later why Tailscale works there.
Join the container to your tailnet
With the device in place, bring the node up. On Tailscale’s hosted service plain tailscale up prints a login URL; we added --login-server because our control server was Headscale, and --hostname so the node name doesn’t depend on the container’s hostname. Inside CT 200:
tailscale up --login-server http://10.10.10.1:8080 --hostname ts-router
The command blocks until you authorise the node, then prints Success. and returns. Run it in a shell you can leave open, because closing it before approval means you start the login again with a new URL:
To authenticate, visit:
http://10.10.10.1:8080/register/hskey-authreq-uzGc_Z6juYT1Y_s3mtZTOj4q
Success.
With Headscale the visit step is headscale auth register --auth-id <id from the URL> --user josphat on the control server; on Tailscale’s service you open the URL and sign in. For unattended containers, an auth key passed with --auth-key skips the browser entirely. Confirm the node got its tailnet address:
tailscale status
tailscale ip -4
ip -br addr show tailscale0
The address lands on a real kernel interface, which is the difference the TUN device makes. Every program in the container can now reach tailnet peers with ordinary sockets, so you get no proxy settings and no special flags for ssh or curl.
100.64.0.1 ts-router josphat linux -
100.64.0.1
tailscale0 UNKNOWN 100.64.0.1/32 fd7a:115c:a1e0::1/128 fe80::7bd5:fe25:e389:c5fa/64
Expect Tailscale to take over /etc/resolv.conf inside the container: ours was rewritten with nameserver 100.100.100.100 and a header saying it’s generated by tailscale, and DNS kept resolving normally, including after a container reboot. If you’d rather keep the resolver Proxmox sets for the container, tailscale dns status itself points to tailscale set --accept-dns=false. The same install and join steps work on a full VM or bare metal, as in our Tailscale on Ubuntu 26.04 and Rocky Linux 10 guides.
Turn the container into a subnet router
A subnet router is the most common reason to run Tailscale in an LXC container: one small container gives every tailnet device a path to the rest of the LAN, the Proxmox web UI, and machines that can’t run Tailscale themselves. In a normal setup you advertise the LAN the container’s eth0 already sits on, so you can skip the next command. Our lab advertises a separate, isolated network instead, because that proves the traffic really went through the router: vmbr1 is a bridge with no ports and no host address (defined in the host’s /etc/network/interfaces like vmbr0), and CT 202 web01 sits on it at 10.20.20.30. We gave CT 200 a second interface on that bridge from the host:
pct set 200 --net1 name=eth1,bridge=vmbr1,ip=10.20.20.20/24
Advertise the route from inside the container. To see what current Tailscale does when forwarding is missing, we turned it off first with sysctl -w net.ipv4.ip_forward=0; on your node the forwarding warning only appears if forwarding really is off, while the GRO warning appears either way:
tailscale up --login-server http://10.10.10.1:8080 --hostname ts-router --advertise-routes=10.20.20.0/24
Both warnings print right away, and tailscale status keeps a health check open until forwarding is fixed. That health check is the useful part, because the warnings from tailscale up scroll past once while the status line stays there for anyone who checks the node later.

One detail surprised us. A fresh container on our node already had net.ipv4.ip_forward = 1 but net.ipv6.conf.all.forwarding = 0, because each unprivileged container gets its own network namespace with its own sysctls. Do not rely on whatever the namespace starts with, since that depends on the host and can change after a host reboot. Write both settings to a file in the container so they survive restarts:
echo 'net.ipv4.ip_forward = 1' | tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | tee -a /etc/sysctl.d/99-tailscale.conf
sysctl -p /etc/sysctl.d/99-tailscale.conf
Unprivileged containers are allowed to change these, so sysctl -p succeeds and echoes the new values. Nothing changes on the Proxmox host itself, which is what you want when the same node runs other guests.
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
The second warning is about UDP GRO on eth0, which limits forwarding throughput rather than breaking anything. We expected ethtool to be refused inside an unprivileged container, but the veth belongs to the container’s namespace and the change went through. Install ethtool and apply the settings from Tailscale’s performance guide:
apt install -y ethtool
ethtool -K eth0 rx-udp-gro-forwarding on rx-gro-list off
ethtool -k eth0 | grep -E "rx-udp-gro-forwarding|rx-gro-list"
Both features read back as set. If your container refuses the change instead, the router still works, so treat this as a throughput fix you can come back to.
rx-gro-list: off
rx-udp-gro-forwarding: on
An ethtool change lasts only until the interface is recreated, and a container restart recreates its veth. A small oneshot unit inside the container reapplies it before tailscaled starts. Create the unit file:
nano /etc/systemd/system/tailscale-gro.service
Paste in the unit we ran in the lab:
[Unit]
Description=UDP GRO forwarding for Tailscale on eth0
After=network-online.target
Wants=network-online.target
Before=tailscaled.service
[Service]
Type=oneshot
ExecStart=/usr/sbin/ethtool -K eth0 rx-udp-gro-forwarding on rx-gro-list off
[Install]
WantedBy=multi-user.target
Enable it, then reboot the container from the host to prove the whole stack comes back on its own.
systemctl daemon-reload
systemctl enable tailscale-gro.service
From the Proxmox host, reboot CT 200:
pct reboot 200
Give it 10 to 15 seconds to finish booting, otherwise the check can run before the unit has, then read the device, the sysctl and the GRO flag. IPv6 forwarding is the value to watch, because IPv4 forwarding was already on in a fresh container on our node and so proves nothing about the file:
pct exec 200 -- bash -c "ethtool -k eth0 | grep rx-udp-gro-forwarding; sysctl net.ipv4.ip_forward net.ipv6.conf.all.forwarding; ls -l /dev/net/tun"
All three settings were back after the reboot. The device comes from the dev0 line in the container config, the sysctl from the file in /etc/sysctl.d and the GRO flag from the new unit, so each one has its own place to look if it ever goes missing.
rx-udp-gro-forwarding: on
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
crw-rw-rw- 1 root root 10, 200 Nov 2 16:53 /dev/net/tun
systemctl status tailscale-gro reports inactive (dead) with status=0/SUCCESS afterwards. That’s normal for a oneshot unit with no RemainAfterExit, since it ran, did its job and exited. Running the same tailscale up line again now prints no warnings, and the health check disappears from tailscale status.
Advertised routes do nothing until the control server approves them. On Tailscale’s service, open Machines in the admin console, use the router’s menu to choose Edit route settings and tick the subnet (the same dialog has the exit node switch you’ll need later). On Headscale, headscale nodes list shows each node’s ID (ts-router was 1 here); approve its route by that ID:
headscale nodes approve-routes -i 1 -r 10.20.20.0/24
headscale nodes list-routes
Read the last three columns. Available means the node advertised the route, Approved means the control server accepted it, and Serving means peers are being told to use it, so a route stuck with only Available filled in is still waiting for approval:
Node updated
ID | Hostname | Approved | Available | Serving (Primary)
1 | ts-router | 10.20.20.0/24 | 10.20.20.0/24 | 10.20.20.0/24
Linux clients ignore subnet routes unless you tell them otherwise, so join CT 201 with --accept-routes. Before this, curl from CT 201 to 10.20.20.30 failed, because CT 201 has no interface on vmbr1 and the host has no address there to route through:
tailscale up --login-server http://10.10.10.1:8080 --hostname ts-client --accept-routes
After approving ts-client, the route shows up in Tailscale’s routing table 52 and the isolated web server answers through the router:

The traceroute is the proof that matters: the first hop is 100.64.0.1, the router’s tailnet address, and the second is web01 itself. Both containers sit on the same Proxmox node, so tailscale ping found a direct path over 10.10.10.20:41641 with no relay. The same pattern works for a pfSense box on the tailnet or any other site you want to reach through one node.
Use the container as an exit node
An exit node sends all of a client’s internet traffic out through the container, which is handy for using the home connection from a hotel or café. It needs the same forwarding setup as a subnet router, so CT 200 is already ready. Advertise it from inside the container:
tailscale set --advertise-exit-node
Approve it on the control server. On Tailscale’s service that is the exit node option in the same Edit route settings dialog. In Headscale an exit node is just two more routes, 0.0.0.0/0 and ::/0, and approve-routes replaces the list, so keep the subnet in it:
headscale nodes approve-routes -i 1 -r 10.20.20.0/24,0.0.0.0/0,::/0
On the client, pick the router as its exit node and trace a public address. A trace is a better test than checking your public IP here, because the lab’s client and router share one uplink and would show the same address either way. Run these inside CT 201.
tailscale set --exit-node=ts-router
tailscale status
traceroute -n -m 3 1.1.1.1
tailscale status marks ts-router as the active exit node, and the trace leaves through it before reaching the host’s NAT gateway:

An exit node in a 512 MB container is fine for a few phones and laptops, but it shares the node’s uplink with everything else on it. If you want a dedicated box for travel instead, the GL.iNet exit node setup pairs a home router with a pocket one, and our travel router picks list the models that run Tailscale natively.
Fall back to userspace networking
Sometimes you cannot change the container’s config, for example on a hosting provider’s LXC or a node you do not administer. Tailscale can then run without /dev/net/tun by handling packets in userspace. CT 203 got no device at all; inside it, add the flags to /etc/default/tailscaled, which the Debian package reads on start:
sed -i 's|^FLAGS=.*|FLAGS="--tun=userspace-networking --socks5-server=localhost:1055"|' /etc/default/tailscaled
systemctl reset-failed tailscaled
systemctl restart tailscaled
After joining it with the same tailscale up ... --accept-routes line, the trade-off is easy to see. The node is on the tailnet and tailscale ping works, but the kernel knows nothing about 100.64.0.0/10, so normal tools fail unless they go through the SOCKS5 proxy:

A plain curl http://10.20.20.30/ from CT 203 timed out (exit code 28), while the same request with --socks5-hostname localhost:1055 returned the page through the subnet router. That makes userspace mode fine for a container that only needs to be reachable or to run one proxy-aware tool, and a poor fit for anything that expects ordinary sockets. If you can add the device, add it.
Fix the errors we hit
Each of these came up during the lab run on Proxmox VE 9.2 and Tailscale 1.104.4, with the message copied from the terminal.
CreateTUN(“tailscale0”) failed; /dev/net/tun does not exist
The container has no TUN device. Add it with pct set <CTID> --dev0 /dev/net/tun or Resources, Add, Device Passthrough, then reset and restart tailscaled. The modprobe tun failure logged just before it is a side effect of a built-in TUN driver, not the cause.
tailscaled.service: Start request repeated too quickly
systemd stops retrying after five fast failures, so adding the device alone does not bring the daemon back. Run systemctl reset-failed tailscaled inside the container before systemctl restart tailscaled, or restart the container.
failed to connect to local tailscaled; it doesn’t appear to be running
Any tailscale command prints this when the daemon is down, whatever the reason. Check systemctl status tailscaled and its journal first; in a container the reason is almost always the missing TUN device above.
Warning: IP forwarding is disabled, subnet routing/exit nodes will not work
Printed by tailscale up with --advertise-routes or --advertise-exit-node, and kept as a health check in tailscale status. Set both forwarding sysctls in /etc/sysctl.d/99-tailscale.conf inside the container, not on the Proxmox host, since the container’s namespace has its own values.
Warning: UDP GRO forwarding is suboptimally configured on eth0
Routing works without the fix, only slower under heavy UDP load. The ethtool -K line works inside an unprivileged container, and the oneshot unit above keeps it across container restarts. The same steps on a regular host are in the Fedora Tailscale guide.
Very useful. Added to my Github favorites. Thank you!
Happy for you!