A drop-in Tailscale exit node + subnet router for your homelab — runs as a single, unprivileged Docker container (no
--privileged, noNET_ADMIN, no/dev/net/tun), configured entirely from one.envfile.
Route your devices' internet traffic out through your home connection from anywhere, and reach machines on your home LAN over an encrypted WireGuard mesh — without poking holes in your firewall or exposing any ports.
- 🌐 Exit node, the easy way. Browse from your laptop or phone as if you were home — same public IP, same geo, encrypted end-to-end.
- 🏠 Reach your LAN remotely. Advertise your home subnets (
TS_ROUTES) to hit NAS, printers, dashboards, or your local DNS resolver from anywhere on the tailnet. - 🔒 Unprivileged by default. Userspace (netstack) networking means no elevated
capabilities and no host TUN device — a smaller blast radius than typical Tailscale
container guides.
no-new-privilegesis on. Flip a single flag if you want kernel-mode throughput instead. - 🧩 One-file config. Everything lives in
.env; the compose file is fully parameterized and heavily commented. - 🛠️ Zero-friction ops. A thin
compose-helper.shwrapper gives youup,restart,logs, and friends without rememberingdocker composeincantations. - ♻️ Survives restarts. Node identity persists in a named volume — no re-auth churn.
The primary job is exit node: tailnet clients route their internet traffic through
this container so it egresses from your home network's public IP. Secondarily, TS_ROUTES
makes it a subnet router so clients can reach hosts on your home LAN (e.g. a local DNS
resolver).
flowchart LR
C["Tailnet clients<br/>(laptop, phone, …)"]
subgraph HOST["Docker host"]
TS["tailscale container<br/>userspace / netstack mode<br/><b>exit node</b> + subnet router"]
end
subgraph LAN["Home LAN"]
H["LAN hosts e.g.<br/>NAS · DNS · dashboards<br/>192.168.x.0/24"]
end
NET(["🌐 Internet"])
C -- "encrypted WireGuard tunnel" --> TS
%% PRIMARY: exit-node path — all internet traffic egresses here
TS == "EXIT NODE:<br/>all internet traffic egresses here" ==> NET
%% Secondary: subnet-router path, controlled by TS_ROUTES
TS -. "TS_ROUTES:<br/>reach LAN subnets" .-> H
classDef primary fill:#dcfce7,stroke:#16a34a,stroke-width:3px,color:#000;
classDef routes fill:#dbeafe,stroke:#2563eb,stroke-width:1px,color:#000;
class TS,NET primary;
class H routes;
- Thick green path = the main use: internet egress via the exit node
(
--advertise-exit-nodeinTS_EXTRA_ARGS). - Dashed blue path =
TS_ROUTES: optional subnet routing to LAN hosts; leave it empty to run exit-node-only. Both the exit node and each route must be approved in the admin console before clients can use them.
Requires Docker + the Compose plugin, and a Tailscale account.
git clone https://github.com/jpbaking/homelab-tailscale.git
cd homelab-tailscale
cp .env.example .env # then edit: set TS_AUTHKEY and your TS_ROUTES
./compose-helper.sh up # build/pull, start detached, follow logsThen, in the admin console, approve the advertised subnet routes and the exit node for this machine — they stay off until approved.
- Get an auth key at https://login.tailscale.com/admin/settings/keys. A reusable + pre-approved + tagged key is recommended for unattended containers.
- Configure by copying
.env.exampleto.env(gitignored) and filling it in — see.env.examplefor every variable and what it does. - Run with
./compose-helper.sh up. - Approve the node's routes/exit-node in the admin console.
This image is configured through environment variables — a TLDR of the official Tailscale Docker docs:
| Variable | What it does |
|---|---|
TS_AUTHKEY |
Auth key that logs the node into your tailnet. Required. |
TS_HOSTNAME |
Name the node shows up as in the tailnet. |
TS_STATE_DIR |
Where tailscaled persists login/state inside the container (backed by a named volume so it stays logged in across restarts). |
TS_USERSPACE |
true = userspace/netstack networking (no TUN/NET_ADMIN); false = kernel networking (needs the TUN device + NET_ADMIN, higher throughput). |
TS_ROUTES |
Comma-separated CIDRs to advertise as a subnet router. Empty = exit-node-only. |
TS_ACCEPT_DNS |
Whether this node accepts DNS config pushed by the tailnet. |
TS_EXTRA_ARGS |
Any extra tailscale up flags, e.g. --advertise-exit-node. |
TS_IMAGE_TAG |
tailscale/tailscale image tag to run. |
| File | Purpose |
|---|---|
docker-compose.yaml |
The Tailscale service definition (single service, fully commented). |
.env.example |
Template for .env. Copy it; .env itself is gitignored (holds the auth key). |
compose-helper.env |
Config for the helper script (project name, timeouts, log tail). |
compose-helper.sh |
Wrapper around docker compose with consistent project naming and shorthand commands. |
./compose-helper.sh up # rebuild/pull, start detached, then follow logs
./compose-helper.sh start # start detached (no pull/build)
./compose-helper.sh restart # down then up
./compose-helper.sh stop # stop + remove orphans
./compose-helper.sh down # stop + remove orphans AND named volumes (wipes state!)
./compose-helper.sh logs # follow logsAnything else passes straight through to docker compose, e.g.
./compose-helper.sh ps or ./compose-helper.sh exec tailscale tailscale status.
⚠️ downremoves thetailscalenamed volume, which holds node state. After that the container re-authenticates withTS_AUTHKEYon next start. Usestop/restartfor routine operations.
- State lives in the
tailscalenamed volume at/var/lib/tailscale. Don'tdown -vunless a clean re-auth is intended. - Networking is deliberate. This runs in userspace/netstack mode (
TS_USERSPACE=true), so the/dev/net/tundevice,cap_add: NET_ADMIN, and forwarding sysctls are intentionally commented out — netstack forwards subnet-router and exit-node traffic in userspace without them. The cost is throughput, not capability. To switch to kernel networking for line-rate forwarding, setTS_USERSPACE=falseand uncomment thedevices,cap_add, andsysctlsblocks indocker-compose.yaml(drop the ipv6 forwarding sysctl if the host has IPv6 disabled).NET_RAWis never needed. - Split DNS for LAN hostnames (resolve
*.lanetc. through a home resolver) is set up in the Tailscale admin console (DNS → restricted nameserver), not in this repo. It rides on an advertised+approved subnet route to that resolver's IP. - Linux clients must run
tailscale up --accept-routesto use advertised subnets; GUI clients accept them by default. compose-helper.shis intended for local/homelab use —downdeletes volumes and.envis sourced into the process. Review before using it in CI.- Changing advertised routes or exit-node status requires re-approval in the admin console.
0BSD — do whatever you want with it, no attribution required.
Built on Tailscale's official
tailscale/tailscale image. See their
Docker how-to
and example configs for more.