Tailscale Serve:在 tailnet 内发布 HTTPS 服务

Tailscale Serve 是 tailnet 内的内置反向代理,把本地 HTTP 服务以自动签发的 HTTPS 证书发布给 tailnet 设备,免域名、免公网暴露。讲清它的 TLS 终止、证书来源、与 Caddy/Cloudflare Tunnel 的差异,以及 443 被占用、重启失效等坑。

#type / concept #status / growing #tech / network #resource / tailscale #platform / server

[!info] related notes

Tailscale Serve:在 tailnet 内发布 HTTPS 服务

一句话定义

Tailscale Serve 是 Tailscale 客户端自带的反向代理:它把本机的一个 HTTP 服务,用 Tailscale 自动签发的 HTTPS 证书,发布给同一 tailnet 内的设备访问。

它解决的是一个听起来很普通、实则处处是坑的问题:怎么给一个只跑 HTTP 的本地服务(比如 noVNC、Web 面板)加上可被浏览器信任的 HTTPS,同时不暴露公网?

为什么需要它:HTTPS 的隐含前提

一个”能被浏览器信任的 HTTPS”必须解决两件事:

  1. TLS 终止:把加密流量解开,交给后面的 HTTP 服务;
  2. 可信证书:证明”我是 example.com / xxx.ts.net”,且证书来自浏览器信任的 CA。

第 2 点是硬门槛——证书不可能凭空出现。常规路线要么买域名 + Let’s Encrypt,要么自己造自签证书(浏览器弹警告)。而 Tailscale Serve 直接白送了第 2 点:它用 Tailscale 自己的 CA 给 *.ts.net 域名签发证书,tailnet 内的设备天然信任。

[!tip] 所以”容器外还要跑一条命令”不是冗余,而是把”证书 + TLS 终止 + 安全隔离”这一整坨本来要做的事,压缩成一条命令。

工作原理

flowchart LR
    A[你的浏览器<br/>tailnet 内] -->|HTTPS 自动证书 *.ts.net| B[tailscaled<br/>Serve 代理]
    B -->|HTTP 本地回环| C[本地服务<br/>127.0.x.x:3010]
  • 浏览器访问 https://<host>.ts.net:<port>,TLS 由 tailscaled 终止(Serve 模块);
  • tailscaled 把请求以明文 HTTP 转发到本机的 127.0.x.x:<localport>
  • 证书由 Tailscale CA 自动签发,tailnet 设备已内置该 CA,浏览器不报警;
  • 默认 tailnet only:只有 tailnet 内设备可达,公网不可见

基础命令

# 把本地 3010 以 HTTPS 发布到 tailnet,对外用 8443 端口
sudo tailscale serve --bg --https=8443 http://127.0.x.x:3010

# 查看状态
sudo tailscale serve status

# 关闭
sudo tailscale serve off

参数说明:

  • --https=<port>:对外 HTTPS 监听端口(不写则默认 443);
  • --bg后台常驻;不加则前台运行,进程被杀后配置即失效;
  • 目标 http://127.0.x.x:<port> 是 Serve 要转发的本地 HTTP 服务。

与另外两种 HTTPS 收口方案对比

维度Tailscale ServeCaddy / Nginx 反代Cloudflare Tunnel
需要自己的域名❌ 不需要(*.ts.net 自带)✅ 需要✅ 需要(托管到 CF)
证书来源Tailscale CA 自动签Let’s Encrypt 自动Cloudflare 边缘证书
公网暴露❌ tailnet only,零公网端口⚠️ 要开 443 入站❌ 仅出站隧道
客户端要求装 Tailscale + 同 tailnet任意浏览器任意浏览器
配置量一条命令一份 Caddyfile + DNScloudflared + 后台开关
大陆可达性WireGuard,偶尔走 DERP 中继直连公网CF 免费 anycast 常被墙/限速

[!note] 选型直觉

  • 只想自己/小团队安全访问 → Tailscale Serve 最省事;
  • 想用自定义域名 + 公共浏览器访问 → Caddy(需域名);
  • 想套 Cloudflare Access 统一鉴权 → Cloudflare Tunnel(但大陆连通性需实测)。

实战中的坑(都已踩过)

坑 1:443 端口被占用 → 改用其它端口

tailscale serve 默认监听 443。若本机 443 已被其它容器/服务占用(如另一 Docker 容器的 docker-proxy),Serve 会撞到那个服务的错证书,报 TLS alert, internal error——这不是 tailscaled 的 bug,是 443 被抢

[!fix] 解法 改用空闲端口(如 8443)即可:

sudo tailscale serve --bg --https=8443 http://127.0.x.x:3010

访问地址变为 https://<host>.ts.net:8443

坑 2:需 tailnet 管理员先启用 Serve 功能

首次在节点上 tailscale serve 可能被 tailnet 的「Serve 功能开关」挡住,提示去后台点启用。这一步必须人工在浏览器点(链接形如 https://login.tailscale.com/f/serve?node=...)。

坑 3:--bg 忘了加 → 配置随进程退出消失

不加 --bg 时 Serve 跑在前台,我的 SSH 命令一结束(或被 timeout 杀掉),代理进程就没了,下次 serve status 为空。务必加 --bg

坑 4:证书首次签发需要等几秒

Serve 起来后立刻 curl 可能仍报 TLS internal error——证书是首次现签的。等 10~30 秒再测,或显式触发一次 sudo tailscale cert <host>.ts.net 加速。

坑 5:重启后 Serve 多半要重跑

--bg 进程不是 systemd 托管的,机器重启后 Serve 不会自动恢复。见下文的固化方案。

固化:让 Serve 开机自启

把 Serve 写成 systemd 服务,避免每次手动跑:

# /etc/systemd/system/tailscale-serve-browser.service
[Unit]
Description=Tailscale Serve for remote browser
After=network-online.target tailscaled.service
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/tailscale serve --bg --https=8443 http://127.0.x.x:3010
ExecStop=/usr/bin/tailscale serve off
User=root

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now tailscale-serve-browser

[!warning] 注意 Tailscale IP 漂移 若容器绑死在某个 Tailscale IP(如 100.68.x.x),而该 IP 日后变更,容器会因绑不到旧 IP 起不来。届时在节点上 tailscale ip -4 取新 IP,改 docker-compose.ymldocker compose up -d,Serve 目标也要相应调整。更稳妥的做法是让容器绑 127.0.x.x,由 Serve 收口(见 部署实战)。

创建于 2026/7/31 更新于 2026/7/31