TailGuard: a container that puts WireGuard hosts on your tailnet
Easy Tailscale to WireGuard bridge in a container
At a glance
- What is it?
- TailGuard is a Docker container that holds a WireGuard tunnel open on one side and a Tailscale node on the other, so a device that cannot run Tailscale binaries can still join a tailnet. It works by keeping a single persistent tunnel and routing the tailnet's traffic through it.
- Who is it for?
- TailGuard fits someone whose WireGuard server sits on a locked-down VPS that cannot install Tailscale, and who wants to reach that network from a phone or a device that only speaks Tailscale. It does not fit a setup where the WireGuard host can simply run the Tailscale client, because the README says direct connection is easier in most cases.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly Shell, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The topology is a single tunnel that both sides hang off
The README draws the network topology as an ASCII diagram, and it is worth reading because the shape explains the whole design. Tailscale clients sit on the left, a VPS in the middle carries TailGuard, and WireGuard hosts sit on the right, with TailGuard speaking both protocols. The README then explains what that means in practice. The tailnet is virtual and in reality connections are point-to-point, but all connections to WireGuard are tunneled through the TailGuard server with a fixed and persistent connection. So the requirement is a server as close to the WireGuard host as possible, ideally with a minimal ping, for example a VPS. Given that, you can connect any WireGuard device to your tailnet. The key word is persistent. TailGuard is not a per-flow tunnel between the two networks, it is a long-lived link with traffic forwarded over it, which is what lets a phone that only speaks Tailscale reach a home network that only speaks WireGuard.
Why you would bridge instead of installing Tailscale directly
The README is honest that for most use cases it may be easier to connect a device with Tailscale or WireGuard directly, and then lists the cases where the bridge earns its complexity. The WireGuard tunnel private key is stored only on a single machine, which makes key management less work. A new device can simply log in to the tailnet with SSO, with no key transfer. It is easier to switch between exit nodes in the tailnet without reconnecting to different VPNs. You get access to both your tailnet and WireGuard concurrently on a mobile device, which does not support multiple VPNs at once. You can temporarily share access to the WireGuard tunnel with other people over Tailscale without sharing any keys. And you can run a server connected to both sides, serving each equally. The list ends with the case that names the project: you can create a WireGuard server that lets devices which do not support Tailscale connect to the tailnet, including using tailnet exit nodes.
Starting it: a WireGuard config, an IPv6 network, and one run command
The simplest way to start is to download a WireGuard config and save it as wg0.conf. After that you can create an IPv6 network, which the README marks optional but recommended, and start the container.
docker network create --ipv6 ip6net
docker run -it \
-v ./wg0.conf:/etc/wireguard/wg0.conf -v ./state:/tailguard/state \
--cap-add NET_ADMIN --device /dev/net/tun \
--sysctl net.ipv4.ip_forward=1 --sysctl net.ipv6.conf.all.forwarding=1 \
--sysctl net.ipv4.conf.all.src_valid_mark=1 \
--network ip6net -p 41641:41641/udp \
--name tailguard ghcr.io/juhovh/tailguard:latestThe flags are the interesting part. NET_ADMIN is needed for the tunnel and the routing, /dev/net/tun is the TUN device, the three sysctls enable IPv4 and IPv6 forwarding and the source valid mark that policy routing needs, and the UDP port is the Tailscale port the container listens on. Docker prints a URL to log in to your tailnet, and after that it works. If you want to build the latest image yourself, the README suggests docker compose, with the wg0.conf file stored under config, then docker compose build and docker compose up.
AllowedIPs is the setting that decides whether routes work
The advanced settings section starts with a concrete example, a WireGuard server on 10.1.0.1 that accepts any routes and whose local LAN is 192.168.8.0/24. The instruction is that you must explicitly mention the subnet 192.168.8.0/24 in the AllowedIPs section in addition to 0.0.0.0/0, for TailGuard to pick it up, and the example config shows the shape of it.
[Interface]
PrivateKey = <REDACTED>
Address = 10.1.0.2/24,fd00:ed7c:a960:6e9b::2/64
DNS = 10.1.0.1,fd00:ed7c:a960:6e9b::1
MTU = 1420
[Peer]
PublicKey = <REDACTED>
PresharedKey = <REDACTED>
AllowedIPs = 0.0.0.0/0,::/0,192.168.8.0/24
Endpoint = <REDACTED>:51820
PersistentKeepalive = 25The allowed IPs line carries the LAN subnet alongside the catch-all routes, and that is the line TailGuard reads to learn what to advertise. The configuration also shows an IPv6 address on the interface and in the DNS list, which is why the container wants an IPv6-enabled Docker network. The MTU of 1420 is set for the extra encapsulation the bridge adds over the tunnel.
The environment variables, and the ones that change behaviour quietly
The container is configured entirely through environment variables, and the list is long enough to be worth scanning for the defaults that matter. TG_EXPOSE_HOST set to 1 allows connections from Tailscale and WireGuard peers. TG_CLIENT_MODE set to 1 makes the container behave as a Tailscale client, which is what allows setting an exit node. TG_NAMESERVERS is the fallback for resolving hostnames, defaulting to Cloudflare 1.1.1.1. TG_WEBUI_PORT is the TCP port for the status UI and the firewall is opened automatically. WG_DEVICE names the WireGuard device and must match the config file name, defaulting to wg0, and WG_ISOLATE_PEERS set to 1 isolates WireGuard peers from each other. On the Tailscale side, TS_DEVICE names the Tailscale device with a default of tailscale0, TS_PORT is the port that Docker should expose with a default of 41641, TS_LOGIN_SERVER overrides the control server, TS_AUTHKEY supplies an auth key, TS_HOSTNAME sets the hostname, TS_DEST_IP is the destination to route Tailscale traffic to over IPv4 and IPv6, TS_ROUTES overrides the autodetected routes, and TS_EXIT_NODE sets an exit node, which only works with client mode enabled. The README notes that some of these follow the naming of the Tailscale Docker image parameters, in which case they should work the same way.
The web UI, and a build that patches Tailscale itself
TailGuard serves a single web page that can be used to monitor both Tailscale and all the WireGuard tunnels configured. It is not enabled by default, and you turn it on with the TG_WEBUI_PORT environment variable. One detail is worth knowing: you do not need TG_EXPOSE_HOST for the web UI, and the port is reachable from both the Tailscale and the WireGuard networks, but if you have set TS_DEST_IP to route Tailscale traffic elsewhere, you cannot reach the web UI through the corresponding Tailscale IP. The build is the other place where the project is unusual. The Dockerfile starts from a Go Alpine image, clones wireguard-tools at a pinned tag for the reresolve-dns.sh script, and clones a pinned Tailscale release, applies a directory of custom patches from tailscale-patches with some lifted from a Tailscale pull request, and builds the tailscale, tailscaled and containerboot binaries with version stamps. The runtime image is Alpine and installs ethtool, iptables, ip6tables, ipcalc, curl, wireguard-tools and wireguard-go, and patches the resolvconf restart command so the container does not try to restart it. The examples directory carries a simple bridge, a simple server, and scripts for using Tailscale or WireGuard as an exit node.
Editorial conclusion
TailGuard fits someone whose WireGuard server sits on a locked-down VPS that cannot install Tailscale, and who wants to reach that network from a phone or a device that only speaks Tailscale. It does not fit a setup where the WireGuard host can simply run the Tailscale client, because the README says direct connection is easier in most cases. Verify first that the AllowedIPs in your WireGuard config name the LAN subnet behind the tunnel, since the README says TailGuard will not pick it up otherwise, and read the environment list before starting, because several defaults, including the nameserver fallback and the isolation of WireGuard peers, change behaviour silently.
Frequently asked questions
What does TailGuard do and when would I need it?
It is a Docker container that connects existing WireGuard hosts to a Tailscale network, for devices that are locked in or do not support Tailscale binaries. The README lists the benefits: the WireGuard private key lives on one machine, new devices join by SSO, exit node switching is easier, and a mobile device that supports only one VPN can reach both networks.
How do I start TailGuard?
Save your WireGuard config as wg0.conf, optionally create an IPv6 Docker network with docker network create --ipv6 ip6net, and run the container with NET_ADMIN, /dev/net/tun, the three forwarding sysctls and the 41641 UDP port published. Docker then prints a URL to log in to your tailnet.
Why are my routes not advertised?
The README says the LAN subnet behind the WireGuard tunnel must be explicitly mentioned in the AllowedIPs section in addition to 0.0.0.0/0, for TailGuard to pick it up. The example config shows 192.168.8.0/24 added alongside the catch-all routes.
How do I enable the TailGuard web interface?
Set the TG_WEBUI_PORT environment variable, for example TG_WEBUI_PORT=8080, and the firewall port is opened automatically. The page monitors both Tailscale and the configured WireGuard tunnels, and is reachable from both networks, but not through a Tailscale IP if you have set TS_DEST_IP to route traffic elsewhere.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/juhovh-tailguard)Was this useful?