Open https://jellyfin.your-tailnet.ts.net instead of 192.168.1.20:8096. This guide shows how Tailscale Services gives each private app its own name, how to configure one, and what to use when you cannot or do not want to use Tailscale.
Many apps on one server covers public domains with Caddy. This guide covers private setups: names that work only inside your own network.
The problem with IP:port
A private app usually runs at an address like http://192.168.1.20:8096. Ten apps require ten numbers in your bookmarks. These addresses also use plain HTTP, and browsers limit some features to HTTPS pages.
You need a name and a certificate for each app. On a Tailscale network, you can run a reverse proxy, add a sidecar to every app, or use Tailscale Services. A sidecar is a helper container that joins Tailscale for one app. Each sidecar shows up as a separate device.
What a Tailscale Service is
A Service provides a name and a virtual IP address on your tailnet. A virtual IP is an address not tied to one machine. The Service belongs to an app, not a device. One or more machines advertise it, and Tailscale routes each client to an active host. If you move the app to another host, the name and address remain the same.
Each Service gets a MagicDNS name like jellyfin.your-tailnet.ts.net. Access rules can name a Service, which lets you grant access to one app without opening the whole host.
Services are generally available on all plans. Each user can create up to 10 Services, as of November 2026.
What you need
- A tailnet, and an Owner, Admin or Network admin role. If you created the tailnet, you are the Owner.
- Tailscale 1.86.0 or later on the host.
- A host that uses a tag instead of a user sign-in. A tag labels a machine, such as
tag:server. User-owned devices cannot host a Service. - HTTPS turned on for your tailnet on the DNS page of the admin console. Check its warning first: certificates go into a public log that lists machine names. Your tailnet name is on the same page. You need it for the final address.
- An app that listens on the host. With Docker, publish it as
"127.0.0.1:8096:8096"underports:so only the host can reach it directly. Docker and Compose for beginners explainsports:.
Clients on 1.94.1 or later need no extra setup. An older Linux client needs sudo tailscale set --accept-routes.
Set up one Service
This example puts Jellyfin behind the name jellyfin. Follow the steps in order: the tag must exist in your policy before a machine can use it.
1. Edit the policy. The policy file holds your tailnet's access rules. Open Access controls in the admin console and copy your current file somewhere safe first. Then add these entries to it. Do not paste over the whole file. A new tailnet starts with an acls list, and you keep it. Grants and legacy ACLs can coexist in one file.
Add these two keys at the top level, next to the existing keys:
"tagOwners": {
"tag:server": ["autogroup:admin"]
},
"autoApprovers": {
"services": {
"svc:jellyfin": ["tag:server"]
}
}
If your file already has a grants list, add this item inside it. If not, add a new top-level grants key that holds the item:
{
"src": ["autogroup:member"],
"dst": ["svc:jellyfin"],
"ip": ["443"]
}
tagOwners defines who can assign the tag. autoApprovers lets a tag:server machine advertise the Service without manual approval. The grant lets every tailnet member reach port 443 of the Service. You need it even if your old rules allow everything: Tailscale says users and devices must explicitly be granted access to a Service through a grant. These blocks match the Tailscale docs.
2. Tag the host. Open the Machines page, open the menu on the server's row, choose Edit tags, add tag:server and save. The tag must already exist in your policy, which is why step 1 comes first. The tags guide also shows a command line way: run sudo tailscale login --advertise-tags=tag:server on the server. It prints a sign-in link, so open it and sign in as an admin. Tagging removes the user account from the machine. Tailscale turns key expiry off for a tagged device by default, but check the server's row. If it shows an expiry date, disable key expiry there, or the server drops off your tailnet when the key expires.
If you reach this server with Tailscale SSH, add an SSH rule before you tag. The default SSH rule covers only your own user devices, and tagging removes the user. Add this to your ssh list:
{"action": "accept", "src": ["autogroup:member"], "dst": ["tag:server"], "users": ["autogroup:nonroot", "root"]}
3. Define the Service. In the admin console, open Services, choose Advertise, then select Define a Service. Enter the name jellyfin and the endpoint tcp:443, then select Add service.
4. Point the Service at the app.
sudo tailscale serve --service=svc:jellyfin --https=443 127.0.0.1:8096
5. Check it. Run tailscale serve status --json on the host. Open https://jellyfin.your-tailnet.ts.net from another device on the tailnet, using your own tailnet name. Serve for a Service runs in the background and resumes after a reboot. Reboot the host once to confirm.
If a host requires approval, open the Services page, pick the Service, and approve the pending host.
What the app sees
The client requests the name. Tailscale sends traffic to a host. The host ends the HTTPS connection and passes plain HTTP to the app. According to one hands-on report, it also adds an X-Forwarded-For header, so the app treats Tailscale as a reverse proxy. The author of that report also wrote DockTail, described below.
Some apps block requests from a proxy until you trust it. Home Assistant is one. In its settings, open System, then Network, then the HTTP server section. Turn on Trust X-Forwarded-For and add 127.0.0.1 to Trusted proxies. If the app runs in Docker, use the address the app names in its log instead. This follows the Home Assistant docs. The same report saw a 400: Bad Request page before that step. It also says Jellyfin works without its Known proxies setting.
Docker: let a watcher do the work
Each Service needs a definition and a serve command. This works for a few apps. A container's internal IP address can change when you recreate it, which breaks a serve command pointing at it.
DockTail automates this setup. It is an independent open-source project under the AGPL-3.0 license, not built by Tailscale. It runs as a single container on the host, watches Docker, and reads labels from your Compose file. This is its Quick Start with a Jellyfin container added:
services:
docktail:
image: ghcr.io/marvinvr/docktail:latest
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/run/tailscale:/var/run/tailscale
environment:
- TAILSCALE_OAUTH_CLIENT_ID=${TAILSCALE_OAUTH_CLIENT_ID}
- TAILSCALE_OAUTH_CLIENT_SECRET=${TAILSCALE_OAUTH_CLIENT_SECRET}
jellyfin:
image: jellyfin/jellyfin
labels:
- "docktail.service.enable=true"
- "docktail.service.name=jellyfin"
- "docktail.service.port=8096"
- "docktail.service.service-port=443"
Steps 1 and 2 still apply: DockTail assumes the host is on Tailscale and allowed to advertise Services. The app needs no published host port, because DockTail proxies to the container's network address.
The two OAuth lines are optional but recommended. An OAuth client is a credential created in the Tailscale admin console that lets a tool act with limited rights. With it, DockTail creates the Service definitions for you. Without it, you still define each Service by hand in step 3. The It's FOSS report used the scopes Services: Write and Devices Core: Write, limited to your server tag.
DockTail can remove unused Service definitions, but that cleanup is opt-in. The Docker socket lets a program control Docker, and a read-only mount still allows broad control of the host. Read the README before you trust it.
Limits to know
- Ten Services per user. Past ten apps, use Caddy with your own domain, as described below.
- TCP only. Services carry TCP. UDP works only in layer 3 mode, a Linux-only mode that forwards raw IP packets. Apps that need UDP do not fit.
- A tagged host counts toward your tagged resources. The free Personal plan includes 50 tagged resources as of November 2026. More cost $1 per month each.
- Drain before you remove. Draining tells a host to stop taking new connections. Run
tailscale serve drain svc:jellyfinbefore you stop the Service, or open connections drop. - Headscale. The Headscale feature list does not include Services as of November 2026. Use an alternative below.
Other ways to get private names
You can get names without Services. These methods work now, and several do not require a Tailscale account.
Tailscale Serve on each machine. Run sudo tailscale serve --bg 3001. Serve assigns an HTTPS address based on the machine name, like https://homeserver.your-tailnet.ts.net. It needs no tags and counts no Services. Each machine gets only one name, so extra apps use other HTTPS ports, such as --https=8443, or paths.
A reverse proxy on a private host. Run Caddy or Traefik on a machine in your network. Add a public wildcard DNS record, like *.home.example.com, that targets the machine's private address. Anyone can resolve the record, but only devices on your private network can reach the address. Caddy obtains certificates through a DNS challenge without opening inbound ports. This setup requires a Caddy build with your DNS provider's module. Many apps on one server covers the Caddy configuration. This works on Tailscale, Headscale, NetBird or WireGuard, and it fits best when you have more than ten apps.
A Tailscale sidecar per container. The official Docker image makes each container a separate tailnet device. Define TS_HOSTNAME for the name, TS_AUTHKEY to connect, TS_STATE_DIR on a volume to keep state across restarts, and TS_SERVE_CONFIG for HTTPS. This works, but adds many devices to your Machines list.
Headscale. Add names directly with dns.extra_records. It supports only A and AAAA records.
dns:
extra_records:
- name: "jellyfin.home.example.com"
type: "A"
value: "100.64.0.3"
Point the name at your proxy host. Get certificates with the DNS challenge above.
NetBird. Custom DNS zones push A, AAAA and CNAME records to chosen peer groups. Point those records at a proxy on one peer. See NetBird.
Pangolin. Private resources support aliases, which give readable internal names to users on the Pangolin client. See Pangolin.
Plain WireGuard. With wg-easy, install a local DNS server like Pi-hole or AdGuard Home. Add one record per name, and point each at your proxy.
To expose a public name behind a login, use a tunnel instead. Choose your front door compares these options.
Which one to pick
- One app on one machine: use Serve.
- Up to ten apps on Tailscale: use Services. This is the default. Consider DockTail if you run Docker.
- More than ten apps, or Headscale, NetBird or plain WireGuard: run Caddy with a DNS challenge and point private DNS records at it.
If something fails
- The Service does not appear. Check the server's row on the Machines page and confirm it has your tag. Without an auto-approver, find the host on the Services page and approve it manually.
- The page says 400 Bad Request. The app rejects the proxy header. Add
127.0.0.1to the app's trusted proxy settings. If the app runs in Docker, use the address the app names in its log instead.
Next: Before you share an app.