- Go 35.2%
- Shell 32.5%
- JavaScript 14.8%
- CSS 9.6%
- HTML 2.9%
- Other 5%
| static | ||
| Dockerfile | ||
| entrypoint.sh | ||
| go.mod | ||
| handlers.go | ||
| justfile | ||
| LICENSE | ||
| main.go | ||
| mullvad-switcher.service | ||
| mullvad.go | ||
| README.md | ||
| routing.sh | ||
| setup.sh | ||
| supervisord.conf | ||
| tailscale-up.sh | ||
| wireguard.go | ||
Mullvad-Tailscale Exit Node
A Docker container that turns a Mullvad WireGuard VPN into a Tailscale exit node, with a web UI to switch between Mullvad server locations.
Any device on your tailnet can use this as an exit node to route all traffic through Mullvad.
How it works
Tailscale clients → tailscale0 → wg0 → Mullvad → Internet
The container runs in its own network namespace with three processes managed by supervisord:
- tailscaled — creates the
tailscale0interface, advertises as an exit node - mullvad-switcher — web UI for switching Mullvad servers
- WireGuard —
wg0tunnel to Mullvad
Policy-based routing (fwmark) steers traffic arriving on tailscale0 through wg0, while the container's own control-plane traffic (Tailscale coordination, API calls) uses the default Docker network.
Prerequisites
- Docker with
NET_ADMINandSYS_MODULEcapabilities - A Mullvad account with a WireGuard config (
wg0.conf) - A Tailscale account with an auth key (or interactive login)
Getting a Mullvad WireGuard config
- Go to mullvad.net/en/account/wireguard-config
- Generate a key and download a config file
- Remove the
DNSline from the config (the container uses Docker's DNS) - Add
PersistentKeepalive = 25to the[Peer]section - Ensure
Table = offis in the[Interface]section (routing is handled by the container)
Your wg0.conf should look like this:
[Interface]
MTU = 1420
Table = off
PrivateKey = <your-private-key>
Address = <your-address>/32
[Peer]
PublicKey = <server-public-key>
AllowedIPs = 0.0.0.0/0,::0/0
Endpoint = <server-ip>:<port>
PersistentKeepalive = 25
Quick start
# docker-compose.yml
services:
mullvad-tailscale:
image: mullvad-tailscale:latest
container_name: mullvad-tailscale
cap_add:
- NET_ADMIN
- SYS_MODULE
devices:
- /dev/net/tun:/dev/net/tun
sysctls:
- net.ipv4.ip_forward=1
- net.ipv6.conf.all.forwarding=1
volumes:
- tailscale-state:/var/lib/tailscale
- ./wg0.conf:/etc/wireguard/wg0.conf
environment:
- TS_AUTHKEY=tskey-auth-xxxxx # from https://login.tailscale.com/admin/settings/keys
- TS_HOSTNAME=mullvad-exit # optional, sets the Tailscale hostname
restart: unless-stopped
volumes:
tailscale-state:
# Place your wg0.conf next to docker-compose.yml, then:
docker compose up -d
The container will:
- Bring up the WireGuard tunnel to Mullvad
- Set up routing rules
- Start Tailscale and authenticate with your auth key
- Advertise itself as an exit node
Without an auth key
If you don't provide TS_AUTHKEY, you'll need to authenticate interactively:
docker compose up -d
docker exec -it mullvad-tailscale tailscale up --advertise-exit-node
# Follow the printed URL to authenticate
Approve the exit node
After authentication, approve the exit node in the Tailscale admin console — click the machine, then enable "Use as exit node".
Building the image
# Build for arm64 (default)
docker build --platform linux/arm64 --load -t mullvad-tailscale:latest .
# Build for amd64
docker build --platform linux/amd64 --load -t mullvad-tailscale:latest .
Web UI
Once running, the switcher web UI is available at http://<tailscale-ip>:8080 from any device on your tailnet.
From the UI you can:
- See the current Mullvad server and connection status
- Browse available servers by country and city
- Switch to a different server with one tap
Environment variables
| Variable | Default | Description |
|---|---|---|
TS_AUTHKEY |
(empty) | Tailscale auth key for headless setup |
TS_HOSTNAME |
(empty) | Tailscale machine hostname |
Architecture
┌─────────────────────────────────────────────┐
│ Container (isolated network namespace) │
│ │
│ ┌───────────┐ ┌────────┐ ┌───────────┐ │
│ │ tailscaled│ │ mullvad │ │ WireGuard │ │
│ │ │ │-switcher│ │ (wg0) │ │
│ └─────┬─────┘ └────┬───┘ └─────┬─────┘ │
│ │ │ │ │
│ tailscale0 :8080 web Mullvad VPN │
│ │ │ │
│ └──── fwmark routing ───────┘ │
│ │
│ eth0 (Docker bridge, control plane only) │
└─────────────────────────────────────────────┘
License
MIT