告别记忆端口之苦:用Tailscale Service统一管理30个Docker容器

Mark Do · · 14 次阅读 · linux软件

告别记忆端口之苦:用Tailscale Service统一管理30个Docker容器

从一个简单的问题说起

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

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

Tailscale帮我解决了随时随地接入内网的问题,但一旦连入网络,我又得回到记忆中:哪个服务跑在哪台机器、哪个端口……

常见的解决方案都有各自的缺陷。用自己的域名搭建反向代理可行,但多了一套维护工作,每上新应用就得同时改两份配置文件。给每个容器都挂一个Tailscale sidecar虽然能得到优雅的名字,却会让主机页面变成一长串容器列表,每个都有自己的认证密钥和状态卷。

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

当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协议),你可以自由使用和改进它。


关注微信号:智享开源 ,及时了解更新信息。

阅读原文

Mark Do

关注我的头条 不要放弃,百折不挠,坚强、自信。

还没有任何评论,你来说两句吧!

发表评论