A CGO-free Docker image of tsvpn: a Tailscale exit-node helper that sends mesh internet traffic through one or more WireGuard VPN tunnels (Surfshark, Mullvad, Proton, …).
This image is the server binary only (make build-server, no GTK tray). It does not include Tailscale. Pair it with a kernel Tailscale sidecar (or any netns that already has tailscale0).
Platforms: linux/amd64, linux/arm64
Tag: heyfworld/tsvpn:latest
51820)100.64.0.0/10) and Docker (172.16.0.0/12) on the main tableprioritytsvpn/
├── docker-compose.yml
├── .env # chmod 600; never commit
├── config.yaml
└── data/
├── profiles/ # WireGuard .conf + .meta.yaml
└── tailscale/ # sidecar Tailscale state
tsvpn must see a kernel tailscale0. The official Tailscale image defaults to userspace — set TS_USERSPACE=false. Share one network namespace:
name: tsvpn-stack
services:
tailscale:
image: tailscale/tailscale:stable
container_name: tsvpn-tailscale
restart: unless-stopped
hostname: ${TS_HOSTNAME:-tsvpn-exit}
environment:
TZ: ${TZ}
TS_AUTHKEY: ${TS_AUTHKEY:-}
TS_STATE_DIR: /var/lib/tailscale
TS_USERSPACE: "false"
TS_HOSTNAME: ${TS_HOSTNAME:-tsvpn-exit}
TS_EXTRA_ARGS: ${TS_EXTRA_ARGS:---advertise-exit-node}
volumes:
- ./data/tailscale:/var/lib/tailscale
devices:
- /dev/net/tun:/dev/net/tun
cap_add:
- NET_ADMIN
- NET_RAW
sysctls:
net.ipv4.ip_forward: "1"
net.ipv6.conf.all.forwarding: "1"
tsvpn:
image: heyfworld/tsvpn:latest
container_name: tsvpn
restart: unless-stopped
network_mode: service:tailscale
privileged: true
stop_grace_period: 30s
depends_on:
tailscale:
condition: service_started
env_file:
- .env
environment:
TZ: ${TZ}
volumes:
- ./config.yaml:/etc/tsvpn/config.yaml:ro
- ./data/profiles:/etc/tsvpn/profiles
entrypoint: ["/bin/sh", "-euc"]
command:
- |
attempt=0
until ip link show tailscale0 >/dev/null 2>&1; do
attempt=$$((attempt + 1))
[ "$$attempt" -lt 30 ] || exit 1
sleep 2
done
exec /usr/local/bin/tsvpn daemon -config /etc/tsvpn/config.yaml
Do not attach this to a host Tailscale daemon if that host already uses Tailscale for other services. The sidecar is a separate tailnet node.
Create .env next to docker-compose.yml (chmod 600). Never commit live keys.
TZ=
TS_AUTHKEY=
TS_HOSTNAME=tsvpn-exit
TS_EXTRA_ARGS=--advertise-exit-node
| Variable | Default | Description |
|---|---|---|
TZ | (set in .env) | Container timezone |
TS_AUTHKEY | empty | Tailscale auth key for this sidecar node. Create at https://login.tailscale.com/admin/settings/keys . Do not invent. |
TS_HOSTNAME | tsvpn-exit | Tailnet hostname of the sidecar (not the Docker host) |
TS_EXTRA_ARGS | --advertise-exit-node | Extra tailscale up flags |
TS_AUTHKEY must be set before docker compose up. WireGuard private keys stay in ./data/profiles/*.conf, not in .env.
Mount at /etc/tsvpn/config.yaml:
profiles_dir: /etc/tsvpn/profiles
web:
port: 7171
bind: 0.0.0.0
routing:
table_id: 51820
tailscale_ipv4: "100.64.0.0/10"
tailscale_ipv6: "fd7a:115c:a1e0::/48"
docker_cidr: "172.16.0.0/12"
bind: auto runs tailscale ip -4. This image does not ship the Tailscale CLI, so use 0.0.0.0 (listens only inside the sidecar netns).
Each profile is two files in /etc/tsvpn/profiles/. The filename stem is the interface name (max 15 characters).
<name>.conf must have Table = off and DNS = none (tsvpn owns routing):
[Interface]
Address = 10.14.0.2/16
PrivateKey = <redacted>
Table = off
DNS = none
[Peer]
PublicKey = <redacted>
AllowedIPs = 0.0.0.0/0
Endpoint = vpn.example.net:51820
<name>.meta.yaml:
provider: vpn
country: XX
server: "region-1"
priority: 1
health:
method: handshake
interval: 15s
max_handshake_age: 180s
ping_target: "1.1.1.1"
failures_before_failover: 3
priority: 1 is highest for auto-failover. 0 (or omit) = manual switch only.
The daemon exits if the profiles directory has no .conf files. After adding or removing profiles, restart the tsvpn container.
Supported natively. Drop several .conf + .meta.yaml pairs (e.g. name-a, name-b). On start, all tunnels come up; only one is the active default route.
Switch from the dashboard (http://<sidecar-tailscale-ip>:7171) or:
docker compose exec tsvpn tsvpn status
docker compose exec tsvpn tsvpn switch <name>
docker compose exec tsvpn tsvpn off
HTTP, no auth in upstream. Anyone who can reach :7171 on the sidecar Tailscale IP can switch tunnels.
tsvpn daemon Start (must run as root)
tsvpn status Tunnel table
tsvpn list Profiles on disk
tsvpn switch <name> Make that tunnel active
tsvpn off Stop VPN routing (tunnels stay up)
tsvpn import <file.conf> Sanitize + write a profile (interactive)
/dev/net/tun)wg-quick / ip rule)tailscale0 in the same netns (TS_USERSPACE=false)Built from gitlab.com/deivi98/tsvpn with CGO_ENABLED=0 -tags notray. Runtime image is Debian slim + wireguard-tools + iproute2.
Content type
Image
Digest
sha256:7f9696bbf…
Size
35.8 MB
Last updated
14 days ago
docker pull heyfworld/tsvpn