
从一个简单的问题说起
我的家庭实验室由5台机器组成,上面跑了大约30个Docker容器。长期以来,我访问这些服务的唯一方式是一个塞满书签的文件夹,里面全是类似 http://100.101.12.4:8096 这样的地址。

Tailscale帮我解决了随时随地接入内网的问题,但一旦连入网络,我又得回到记忆中:哪个服务跑在哪台机器、哪个端口……
常见的解决方案都有各自的缺陷。用自己的域名搭建反向代理可行,但多了一套维护工作,每上新应用就得同时改两份配置文件。给每个容器都挂一个Tailscale sidecar虽然能得到优雅的名字,却会让主机页面变成一长串容器列表,每个都有自己的认证密钥和状态卷。

当Tailscale推出Services功能时,我终于看到了真正想要的东西:为应用命名,而不是为机器命名。之后我一直在用这套方案,以下是完整的使用说明和安装教程,以及开搞之前值得了解的两件事。
Tailscale Service到底是什么
一个Service就是一个绑定在虚拟IP上的名称,例如 jellyfin.your-tailnet.ts.net。它不属于某台具体设备,而是由一台或多台机器向外宣告,然后Tailscale负责把客户端路由过去。
这意味着你的Docker主机在tailnet中依然是一台机器,而上面的应用才一个个成为独立的Service。访问控制策略可以直接指向某个Service,这样就能只让某个人访问Jellyfin,而不会把整台主机暴露出去。
配置完成后,一个请求的走向是这样的:
- 手机通过Tailscale连接
jellyfin.your-tailnet.ts.net - Tailscale将请求路由到Docker主机
- 主机上的
tailscaled终止HTTPS,转发明文HTTP,并添加X-Forwarded-For请求头 - 流量最终到达Jellyfin容器的Docker网络IP
图中有两个细节值得注意:主机的 tailscaled 承担了HTTPS终止和反向代理的工作,对应用而言Tailscale的行为就像标准的反向代理一样。
手动配置一个Service
在考虑自动化之前,建议先手动配置一次,以熟悉整个流程。你需要准备三样东西。
第一步:给主机打上标签(Tag)。 Service需要通过带标签的机器来宣告,所以先给你的Docker主机打一个标签:
sudo tailscale up --advertise-tags=tag:server --reset
第二步:配置访问控制策略。 在管理控制台的”Access Controls”页面中,确保标签已存在,并配置自动审批规则,省去每次都要点”Approve”的麻烦:
{
"tagOwners": {
"tag:server": ["autogroup:admin"],
"tag:container": ["tag:server"]
},
"autoApprovers": {
"services": {
"tag:container": ["tag:server"]
}
}
}
第三步:创建Service并指向容器。 在控制台Services页面选择”Advertise”→”Define a Service”,命名为 jellyfin,关联 tag:container 标签,然后在主机上执行:
tailscale serve --service=svc:jellyfin --https=443 127.0.0.1:8096
这里假设Jellyfin在主机的8096端口提供服务。如果你的tailnet已启用HTTPS证书(控制台DNS页面可配置),那么从任意安装了Tailscale的设备访问 https://jellyfin.your-tailnet.ts.net 即可正常打开。
对于一个应用来说,手动配置没问题。但对于30个应用,这种做法很快就会让人崩溃——控制台里一份定义,命令行一条 serve 命令,外加重复映射端口。而且当你增删或迁移容器时,这些配置不会自动更新。
为什么最终还是写了脚本
最让我困扰的是配置分散在两个地方:应用定义在Docker Compose文件里,而如何访问它则散落在控制台和 tailscaled 的状态文件中。删掉一个容器后,对应的Service会留在控制台里无人清理。
Traefik多年前就用Docker标签解决过反向代理的类似问题,但我没找到Tailscale Service对应的方案。于是我用Go写了一个小工具——DockTail,它可以作为每台主机上一个独立的容器运行,监控Docker事件,将Label转换为上面提到的配置步骤:
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"
配合具有”Services: Write”和”Devices Core: Write”权限的OAuth客户端(范围限定为 tag:server),DockTail会自动完成服务定义,连控制台的步骤都省掉了。注意Jellyfin的容器中不需要 ports: 映射——流量直接指向容器在Docker网络中的IP。
这也是为什么需要一款能监听Docker事件的工具。如果跳过端口映射,手动把 serve 指向容器IP,容器重建后Docker分配的IP会变化,手动配置就会指向一个失效的地址:
- 容器重建前:IP为
172.18.0.5 - 容器重建后:IP变为
172.18.0.9 - 手动配置的
serve规则仍指向旧IP,导致连接中断 - DockTail这类工具会实时感知Docker事件,自动更新指向
DockTail是开源项目(AGPL-3.0协议),你可以自由使用和改进它。
关注微信号:智享开源 ,及时了解更新信息。
还没有任何评论,你来说两句吧!