Skip to content

Tailscale

A proxy host can be served on your tailnet instead of, or as well as, the public internet - and it needs nothing else on the host. The caddy-tailscale plugin runs a Tailscale node in userspace inside the Caddy process: no tailscaled, no /dev/net/tun, no extra published ports, and no change to your compose file.

Turn it on in Settings → Network → Tailscale, on the Node defaults card. The one thing it needs is a reusable auth key from the Tailscale admin console. If you would rather not store the key in the database, put a Caddy placeholder in the field instead - {env.TS_AUTHKEY} is passed through untouched and resolved from the container’s environment.

Setting What it does
Auth key Registers each node. Encrypted at rest, never sent back to the browser
Default node name The machine name a host inherits when it names none. Several hosts can share one node
Tags ACL tags applied at registration. Most reusable keys need at least one, e.g. tag:caddy
Control server URL Point at Headscale or another coordination server. Empty uses Tailscale’s own
State directory Where each node keeps its identity. Keep it on a volume, or every restart registers a new machine
Register nodes as ephemeral Nodes leave the tailnet when Caddy stops rather than lingering as offline machines
Serve HTTP/3 on tailnet listeners Off by default. Adds QUIC to the tailnet listeners, if HTTP/3 is also on globally

HTTP/2 on the tailnet listeners follows the global switch in Settings → Network → HTTP Versions, with no opt-in of its own.

Serve on tailnet moves the host’s routes to a listener on the chosen node. Tailnet only - on by default - keeps them off the public listener entirely, so the service exists only for devices on your tailnet.

Tailscale options on a proxy hostDemoNothing you change here is saved
TailscaleServe this host on your tailnet instead of, or as well as, the public internet
The tailnet machine this host is served on. Empty uses "caddy" from Settings → Network → Tailscale. Several hosts can share one node.
Add the node's MagicDNS name to Domains
Routing is still by Host header, so a request to https://caddy.your-tailnet.ts.net only reaches this host if that name is one of its domains. Caddy gets the certificate for it from Tailscale - no ACME, no DNS provider.
Keep this host off the public :80/:443 listener entirely. Uncheck to publish it in both places.
Only devices signed in to your tailnet may reach this host, and the caller is identified by their tailnet login. Tagged devices are refused, since they have no user behind them.
Leave empty to require an identity for the whole host. Comma-separated paths gate only those routes.
Paths that bypass the identity check while everything else stays gated. Ignored if Protected Paths is set.
Sets X-Tailscale-User, -Login, -Name, -Tailnet and -Profile-Picture on the proxied request. Any such header sent by the client is stripped first.
Node to dial the upstreams through, for a backend that only exists on your tailnet. Upstream IP pinning and custom DNS resolvers do not apply - names are resolved by MagicDNS on the far side.

Routing is still by Host header, so add the node’s MagicDNS name to the host’s domains. Caddy gets the certificate for that name from Tailscale - no ACME, no DNS provider, nothing to configure. A .ts.net name is never sent to a public CA, which could not validate it anyway.

Require a Tailscale identity admits only devices signed in to your tailnet, identified by their tailnet login, with the same protected and excluded path lists the other authentication integrations use. The identity can be forwarded upstream as headers.

Reach upstreams over the tailnet reaches a service that only exists on the tailnet, without exposing it anywhere else.

A host set to serve only on the tailnet, on a deployment where Tailscale is off or the module is not in the running binary, is left out of the configuration entirely rather than published on the public listener. Falling back would expose a service deliberately kept private.

Separately from serving hosts, an agent can reach its controller across a tailnet - it only needs an outbound route, so point CONTROLLER_URL at the controller’s tailnet address. Pairing, the event stream and long-lived connections behave as they do on a flat network.