> ## Content Index
> Fetch the complete content index at: https://itsfoss.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# I Got Tired of Remembering Ports for 30 Docker Apps in My Homelab. Here's How I Fixed it
- URL: https://itsfoss.com/tailscale-services/
- Published: 2026-11-04T06:36:35.000Z
- Updated: 2026-11-04T07:42:51.000Z
- Description: Stop bookmarking IP:ports. Tailscale Services gives every Docker container in your homelab its own HTTPS name on your tailnet, without a reverse proxy or a sidecar per container.
- Author: Community
- Tags: Homelab

My homelab runs on 5 machines with around 30 Docker containers. For a long time, the way I reached them was a bookmarks folder full of entries like `http://100.101.12.4:8096`. 

[Tailscale](https://tailscale.com/?ref=itsfoss.com) took care of getting me into my network from anywhere, but once I was in, I was back to remembering which box ran what on which port.

The [usual fixes each have a catch](https://itsfoss.com/homelab-internal-domain-setup/). A reverse proxy with your own domain works, but it's one more thing to maintain, and every new app means touching two config files instead of one. Giving each container its own Tailscale sidecar gets you nice names, but it also turns the Machines page into a list of containers, each with its own auth key and state volume.

When Tailscale released [Services](https://tailscale.com/kb/1552/tailscale-services?ref=itsfoss.com), it looked like the thing I had actually wanted: names for *apps*, not for machines. I've been running my containers that way since, and this is a write-up of how it works, how to set it up by hand, and the two things worth knowing before you start.

## What a Tailscale Service actually is

A Service is a name plus a virtual IP on your tailnet, for example `jellyfin.your-tailnet.ts.net`. It doesn't belong to a device. Instead, one or more of your machines *advertise* it, and Tailscale routes clients to them.

So your Docker host stays a single machine in the tailnet, and the apps on it become Services. Access rules can point at a Service directly, so you can let someone reach Jellyfin without giving them the whole host.

Here is what a request looks like once it's set up:

![Request path for a Tailscale Service: a phone on the tailnet opens jellyfin.your-tailnet.ts.net, Tailscale routes it to the Docker host, tailscaled terminates HTTPS and forwards plain HTTP with an X-Forwarded-For header to the Jellyfin container on its Docker network IP](https://itsfoss.com/content/images/2026/11/01-request-path.png)

Two details in that picture matter later: `tailscaled` on the host terminates HTTPS and talks plain HTTP to your app, and it adds an `X-Forwarded-For` header. To your app, Tailscale behaves like a reverse proxy.

## Setting one up by hand

Before automating anything, it's worth doing it manually once for a single container. You need three things.

**1\. A tagged host.** Services are advertised by tagged machines, so give your Docker host a tag:

```bash
sudo tailscale up --advertise-tags=tag:server --reset

```

**2\. A policy that allows it.** In the admin console under Access controls, the tags must exist, and an auto-approver saves you from clicking "Approve" for every Service:

```json
{
  "tagOwners": {
    "tag:server": ["autogroup:admin"],
    "tag:container": ["tag:server"]
  },
  "autoApprovers": {
    "services": {
      "tag:container": ["tag:server"]
    }
  }
}

```

**3\. The Service itself.** On the Services page of the admin console, choose *Advertise*, then *Define a Service*, name it `jellyfin` and give it the `tag:container` tag. Then, on the host, point it at the app:

```bash
tailscale serve --service=svc:jellyfin --https=443 127.0.0.1:8096

```

This assumes Jellyfin publishes port 8096 on the host. If HTTPS certificates are enabled for your tailnet (DNS page in the admin console), `https://jellyfin.your-tailnet.ts.net` now works from every device that runs Tailscale.

For one app, that's fine. For 30, it gets old quickly: one definition in the console and one `serve` command per app, plus published ports again. And nothing updates by itself when you add, remove or move a container.

## Why I ended up scripting it

The part that bothered me most was that the configuration lived in two places. The app was defined in a [Compose file](https://linuxhandbook.com/docker-compose-quick-start/?ref=itsfoss.com), and how to reach it was defined somewhere in the admin console and in `tailscaled`'s state. Remove a container and its Service just stays there.

[Traefik](https://traefik.io/traefik?ref=itsfoss.com) solved the same problem for reverse proxies years ago with Docker labels, and I didn't find anything like that for Tailscale Services. So I wrote a small Go program that does it, [DockTail](https://github.com/marvinvr/docktail?ref=itsfoss.com). It runs as one container per host, watches Docker events, and turns labels like these into the steps above:

```yaml
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:latest
    restart: unless-stopped
    volumes:
      - ./config:/config
      - ./cache:/cache
      - /path/to/media:/media:ro
    labels:
      - "docktail.service.enable=true"
      - "docktail.service.name=jellyfin"
      - "docktail.service.port=8096"
      - "docktail.service.service-port=443"

```

With an OAuth client (Services: Write, Devices Core: Write, scoped to `tag:server`) it also creates the Service definition, so the manual console step goes away. Note that Jellyfin has no `ports:` section: the traffic goes straight to the container's IP on its Docker network.

That's also why this needs something that watches Docker. If you skip published ports and point `serve` at a container IP by hand, it breaks the next time the container is recreated:

![When the Jellyfin container is recreated, its Docker network IP changes from 172.18.0.5 to 172.18.0.9. A serve config written by hand still points at the old address, while a tool that watches Docker events updates it](https://itsfoss.com/content/images/2026/11/02-ip-change.png)

DockTail is open source (AGPL-3.0). You don't need it to use Services, though, and everything in the next section applies either way.

## Two things worth knowing

**If a Service doesn't show up**, it's almost always the tag. The host needs to be tagged (check `tailscale status`), and if you haven't set up an auto-approver, the Service waits for you on the Services page.

**Apps that check proxy headers** may complain at first, because Tailscale adds an `X-Forwarded-For` header. Home Assistant answers with "400: Bad Request" until you trust the proxy in `configuration.yaml`:

```yaml
http:
  use_x_forwarded_for: true
  trusted_proxies:
    - 127.0.0.1

```

On a Docker bridge network, use the address Home Assistant names in its log instead of `127.0.0.1`. Jellyfin has the same setting under *Dashboard → Networking → Known proxies*, but it works fine without it.

## Was it worth switching?

For me, yes. What I like most is that the network config now sits right next to the app in the same Compose file. Adding an app means adding labels, removing it takes its Service with it, and my Machines page shows machines again.

I'd still use a reverse proxy for anything public with my own domain. Tailscale Funnel is great for a webhook or sharing one thing with a friend, but it's not a replacement for that. And if you run a handful of containers that rarely change, the manual `tailscale serve` route above is perfectly fine and needs no extra tool.

If you want to try the label approach, the code and docs (including complete guides for Jellyfin and Home Assistant) are at [github.com/marvinvr/docktail](https://github.com/marvinvr/docktail?ref=itsfoss.com) and [docktail.org/docs](https://docktail.org/docs/?ref=itsfoss.com). 

And if you've solved this differently, I'd honestly like to hear how. Leave a comment, I'll read all of them.

✍️

The article is contributed by Marvin von Rappard, creator of open source project [Docktail](https://docktail.org/?ref=itsfoss.com).