TSDProxy: Tailscale reverse proxy driven by Docker labels
Automatic Tailscale reverse proxy for Docker containers. Zero sidecars. Label-based config. Automatic HTTPS.
At a glance
- What is it?
- TSDProxy turns a Docker container into a Tailscale machine with one label, using tsnet instead of a per-service sidecar. It is a good fit for homelabs and small self-hosted fleets; the trade-off is that the proxy itself becomes the control plane for every exposed service.
- Who is it for?
- Adopt TSDProxy if you run Docker on one host, want Tailscale HTTPS without a sidecar per service, and are comfortable giving the proxy container access to /var/run/docker.sock. Do not adopt it if you need stable non-beta releases, cannot grant Docker socket access, or want a proxy that does not depend on a third-party coordination service.
- 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 8 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The problem: one Tailscale node per exposed service
The default way to put a containerized service on a tailnet is to run Tailscale inside it, or beside it as a sidecar, and let that instance claim its own machine name and certificate. That works, but each service then carries its own Tailscale state, its own authentication, and its own node entry in the admin console. Ten services means ten machines to name, authorize, and later clean up. The README frames TSDProxy as the alternative: "One label. Zero sidecars." A single proxy process holds the Tailscale identity and creates the per-service machines itself through tsnet, the library that lets a Go program act as a Tailscale node without a separate daemon. The intended audience is visible in the repository topics: homelab, self-hosted, docker-compose, traefik-alternative. This is aimed at people running a handful to a few dozen containers on hardware they control, not at platform teams provisioning multi-tenant clusters.
How the tsnet machine lifecycle works
The README's flow diagram and its six numbered steps describe the mechanism precisely enough to reason about. TSDProxy watches the Docker daemon for containers carrying tsdproxy.enable=true. When one appears, the proxy spins up a Tailscale machine via tsnet, taking the hostname from the tsdproxy.name label or falling back to the container name. It then maps the container's internal port to that machine, and incoming requests to https://myapp.<tailnet>.ts.net are reverse-proxied to the container. When the container stops, the machine and its routes are removed. The important consequence is that the number of Tailscale nodes scales with the number of exposed containers, not with the number of proxy processes. You run one TSDProxy; the tailnet still sees one machine per service. That is the design decision to weigh. A single tsnet process multiplexing many machines keeps your compose files small, but it also means one process holds the credentials and lifecycle for every service it fronts. The dashboard, REST API, and health monitoring all attach to that same process.
Getting it running: compose file, labels, dashboard
The quick start is a single compose file. The proxy service mounts /var/run/docker.sock, a named volume for its data, and ./config for configuration, publishes port 8080, and sets extra_hosts with host.docker.internal:host-gateway. A second service, the one being exposed, needs only two labels: tsdproxy.enable set to "true" and tsdproxy.name set to the desired hostname. Running docker compose up -d starts both. According to the README, TSDProxy writes a default config to /config/tsdproxy.yaml on first run, and you then open the dashboard at http://localhost:8080, click the proxy card, and authenticate with Tailscale. After that the service is reachable at https://myapp.<tailnet-name>.ts.net. For unattended setups the README points to AuthKey or OAuth configuration under the advanced Tailscale docs, which matters because the interactive dashboard flow does not fit a headless server you only reach over SSH. Note the image tag in the example: almeidapaulopt/tsdproxy:2 selects the v2 line, while :latest tracks the latest stable release and :dev the development build.
Port labels do more than HTTP
The port label syntax is where the project stops looking like a thin wrapper. The README's example shows tsdproxy.port.1: "443/https:80/http" for HTTPS terminated on 443 forwarded to container port 80, and a second entry for plain HTTP. It also documents a redirect form, tsdproxy.port.3: "81/http->https://myservice.tailnet.ts.net", which sends HTTP visitors to the HTTPS URL rather than proxying them. Beyond HTTP, the same label family covers TCP (the example uses 22/tcp:22/tcp for SSH) and UDP (5060/udp:5060/udp), and a range form, 2222-2230/tcp:2222-2230/tcp, that opens ten consecutive ports from one label. Two things follow. First, TSDProxy is not only an HTTP reverse proxy; it is a port forwarder over the tailnet, which is why database and game-server use cases appear in the topics. Second, the label grammar is dense. A typo in a protocol token or an arrow direction is a silent misconfiguration until you test the port, and the README gives examples rather than a full grammar, so the port configuration docs page is the reference you actually need open while writing labels.
Where it is the wrong tool
The Docker socket mount is the limitation to take seriously. TSDProxy needs /var/run/docker.sock to watch containers, and socket access is effectively root-equivalent on the host. Anyone who can influence the proxy's configuration, or exploit it, inherits that. If your threat model rules out handing the Docker socket to a third-party container, this design fails that test regardless of how convenient the labels are. The second constraint is release maturity. The recent releases listed for this repository are v3.0.0-beta.1, v3.0.0-beta.2, and v3.0.0-beta.3, all dated June 2026, while the quick start still pulls the :2 image. That is a beta line in active flux. The README also links an upgrading guide from v1, which tells you the configuration surface has changed across major versions before and can again. Third, Funnel, the option that exposes a service to the public internet, changes the exposure model entirely; a tailnet-only mental model does not carry over, and the README defers that topic to a dedicated security page rather than explaining it inline. Finally, nothing in the supplied material describes behavior across multiple Docker hosts. The scanning model described is a single daemon.
Compared with running Tailscale per service
The direct alternative is the sidecar pattern: add a Tailscale container to each service's compose definition, or bake tailscaled into the image, and let each service own its node. The difference is where state lives. With sidecars, each service authenticates independently and its tailnet identity is coupled to that container; removing the container removes the node, and a misbehaving service cannot affect its neighbors' networking. With TSDProxy, one process creates and destroys machines on behalf of everything, which is why a container stopping triggers automatic cleanup of its machine and routes, and why a proxy restart touches every service at once. Traefik is the other comparison the repository itself invites with its traefik-alternative topic. Traefik is a general reverse proxy with its own provider and middleware model, and it does not supply the VPN layer; you would still need Tailscale somewhere to get the .ts.net names and certificates. TSDProxy's distinguishing choice is to fuse the proxy and the tailnet node into one process. If you already run Traefik and only need remote access, adding Tailscale to the existing stack is less machinery than introducing a second proxy.
Maintenance, configuration drift, and licence
Operating cost here is mostly version drift. The project is MIT licensed, so forking, vendoring, and internal modification are permitted, and there is no copyleft obligation to publish changes. That also means no vendor is on the hook for support; the README routes users to CONTRIBUTING.md for bug reports and to GitHub sponsorships for funding, which is the usual shape of a single-maintainer project. The configuration surface is the part most likely to cost you time. Settings live in /config/tsdproxy.yaml, the README advertises live config reload without restarting TSDProxy, and there is a REST API for pausing and resuming proxies. Those features reduce restart churn but they also mean the running state of the proxy is not fully described by your compose file or your labels. Health monitoring with backend probes and target re-resolution adds another moving part that can mask a broken container by retrying rather than surfacing the failure. If you adopt it, keep the tsdproxy.yaml under version control alongside the compose file, and pin the image tag rather than tracking :latest, because the gap between the :2 tag and the v3 beta line is where surprises will come from. This is a description of the licence terms, not legal advice.
Editorial conclusion
Adopt TSDProxy if you run Docker on one host, want Tailscale HTTPS without a sidecar per service, and are comfortable giving the proxy container access to /var/run/docker.sock. Do not adopt it if you need stable non-beta releases, cannot grant Docker socket access, or want a proxy that does not depend on a third-party coordination service. Before deploying, verify which release tag you are pulling (the current line is v3.0.0-beta.3), confirm the tsdproxy.port.N label syntax against your version's docs, and decide up front whether authentication will be interactive or headless via AuthKey or OAuth.
Community notes