在 Fly.io 云服务器上部署 Syncthing
我最近发现了 Syncthing,一个用于在多台机器之间同步文件的开源工具。
在我的个人设备上设置 Syncthing 很容易,但把它部署到云服务器上则是一段有趣的旅程。
为什么要在云端运行 Syncthing?
Syncthing 以点对点(peer-to-peer)方式同步文件。这意味着至少要有两台设备同时在线并运行 Syncthing,才能保持同步。如果我在台式机上修改了一个文件,然后关机并带着笔记本电脑出差,我的笔记本就无法获取我在台式机上所做的更改。
如果我有一台始终在线、随时可用的云服务器运行 Syncthing,就可以避免各设备之间失去同步。
我不想听你的人生故事——直接告诉我怎么部署 Syncthing
我会分享几种部署 Syncthing 的失败尝试。如果你想直接跳到解决方案,请参阅“如何将 Syncthing 部署到 Fly.io”一节。
前人工作:Fly.io 上的 Syncthing + Tailscale
过去两年里,Fly.io 一直是我首选的云托管服务商,所以我查了一下是否有人写过关于在 Fly.io 上运行 Syncthing 的文章。结果发现,不到一年前,Andrew Katz(安德鲁·卡茨)写过一篇不错的教程。
Andrew 的教程是个好消息,因为它证明了我的想法是可行的。我唯一的小意见是它依赖于 Tailscale,一个流行的 VPN 解决方案。我很喜欢 Tailscale,但在这种场景下它有一些严重的缺点。
将 Syncthing 与 Tailscale 结合需要构建自定义的 Docker 镜像。随着这两个工具不断演进,更新该镜像是一项不小的维护负担。而且把两个应用混装在一个容器里是Docker 的大忌之一。
所有 Fly.io 服务器默认都内置了 Wireguard VPN。我怀疑可以借助已有的 Wireguard 连接来改进 Andrew 的方案,而不是混入 Tailscale。
linuxserver 的 Docker 镜像无法在 Fly.io 上运行
当我搜索 Syncthing 的 Docker 镜像时,不知怎么就漏掉了官方 Docker 镜像,反而找到了非官方的 LinuxServer.io 版本。
我尝试把 linuxserver.io 版本部署到 Fly.io,但服务器立刻陷入了崩溃循环:
2023-05-23T12:44:17.247 [info] Preparing to run: `/init` as root
2023-05-23T12:44:17.258 [info] 2023/05/23 12:44:17 listening on [fdaa:0:20ad:a7b:cb:a9e9:30cd:2]:22 (DNS: [fdaa::3]:53)
2023-05-23T12:44:17.261 [info] s6-overlay-suexec: fatal: can only run as pid 1我发现一个存在相同问题的 Fly.io 支持帖:
经过一番排查,我想我知道问题出在哪了!多亏你上一条消息中的错误信息
s6-overlay-suexec: fatal: can only run as pid 1我做了一些调查,发现我所用的镜像使用了一个想要以 pid 1 运行的进程管理器,而根据《Running Multiple Processes Inside A Fly.io App》一文,这是不可能的。
查看 linuxserver 的 Docker 源码后,我发现其运行时镜像依赖于 linuxserver/baseimage-alpine。我调出了该镜像的源码,虽然我对覆盖 init 进程了解不多,但文件中有几行与 init 相关的内容,看起来 @mpaupulaire 发现的问题正好解释了我的崩溃循环。
我没有更严格地去找官方的 Syncthing Docker 镜像,而是花了三个小时自己动手做了一个。等到坐下来写这篇教程时,我才意识到自己漏掉了官方镜像,所以我就直接用它了。
Fly.io 上基础的 Syncthing 部署
有了官方 Syncthing Docker 镜像,我就可以开始部署到 Fly.io 了。首先,我创建了一个新的 Fly.io 应用:
$ fly apps create --name syncthing-mtlynch
? Select Organization: Michael Lynch (personal)
New app created: syncthing-mtlynchSyncthing 需要一个存储数据的地方,所以我创建了一个名为 syncthing_data 的 Fly.io 持久卷:
SIZE_IN_GB=3 # This is the limit of fly.io's free tier as of 2023-05-29
fly volumes create syncthing_data \
--size "${SIZE_IN_GB}" \
--yes接下来,我为 Syncthing 编写了一个最小化的 Fly.io 配置:
app = "syncthing-mtlynch"
[build]
image = "syncthing/syncthing:1.23.4"
[mounts]
source="syncthing_data"
destination="/var/syncthing"现在,见证时刻到了。我启动了这个应用:
$ fly deploy
==> Verifying app config
Validating /tmp/tmp.mezhLZdpSv/fly.toml
Platform: machines
✓ Configuration is valid
--> Verified app config
==> Building image
Searching for image 'syncthing/syncthing:1.23.4' remotely...
image found: img_98dgp8mlx504xw05
Watch your app at https://fly.io/apps/syncthing-mtlynch/monitoring
Updating existing machines in 'syncthing-mtlynch' with rolling strategy
[1/1] Replacing 6e82ddd3ae5698 [app] by new machine
[1/1] Machine 918570e1f96283 [app] update finished: success
Finished deploying成功了!从日志来看,Syncthing 已经启动并运行。
2023/05/25 12:09:52 INFO: My ID: YERKMWG-WMUKYOR-J57TFK7-LQ3NHPX-6TI5AFU-IX7SEEW-GX7QO3C-NPYATQT
2023/05/25 12:09:53 INFO: GUI and API listening on [::]:8384
2023/05/25 12:09:53 INFO: Access the GUI via the following URL: http://127.0.0.1:8384/
2023/05/25 12:09:53 INFO: My name is "918570e1f96283"
2023/05/25 12:09:53 INFO: Completed initial scan of sendreceive folder "Default Folder" (default)
2023/05/25 12:10:12 INFO: quic://0.0.0.0:22000 detected NAT type: Port restricted NAT
2023/05/25 12:10:12 INFO: quic://0.0.0.0:22000 resolved external address quic://66.225.222.75:22000 (2023/05/25 12:10:32 INFO: Joined relay relay://54.175.93.212:443日志中显示了 Syncthing 服务器的设备 ID,因此我可以从本地的 Syncthing 服务器把它添加为对等节点:
遗憾的是,我本地的 Syncthing 实例无法连接到云服务器。
这是意料之中的,因为我还没有配置我的 Fly.io 服务器允许任何入站流量。
如果把服务器放置足够长的时间,它很可能会通过中继(relay)自行建立连接,但如果正确配置好入站端口,Syncthing 的表现会更好。
为 Syncthing 配置防火墙端口
此时,Syncthing 已在 Fly.io 上启动并运行,但它无法接收来自我其他任何设备的流量。
Syncthing 提供了非常清晰的文档,说明如何配置防火墙以开放它与对等节点通信所需的端口:
端口 22000/TCP:基于 TCP 的同步协议流量
端口 22000/UDP:基于 QUIC 的同步协议流量
端口 21027/UDP:用于 IPv4 上的发现广播和 IPv6 上的组播
以下是我将其转换为 Fly.io 配置的方式。
[[services]]
internal_port = 22000
protocol = "tcp"
[[services.ports]]
port = 22000
[[services.tcp_checks]]
grace_period = "1s"
interval = "15s"
restart_limit = 0
timeout = "2s"
[[services]]
internal_port = 22000
protocol = "udp"
[[services.ports]]
port = 22000
[[services]]
internal_port = 21027
protocol = "udp"
[[services.ports]]
port = 21027我把端口 22000 用作健康检查端口。这意味着 Fly.io 会定期轮询该端口,如果无法连接,它就会知道 Syncthing 处于不健康状态。
Syncthing Docker 镜像的管理界面默认监听 0.0.0.0:8384,也就是说它在私有和公共网络接口上都接受连接。这其实不应该有影响,因为我的 Fly.io 配置并没有开放端口 8384。不过出于纵深防御的考虑,我还是将 Syncthing 配置为只监听回环接口。
Syncthing 的文档说明,可以通过取消设置 STGUIADDRESS 环境变量来限制对管理界面的访问。
[env]
# Only listen for connections to admin GUI through localhost.
STGUIADDRESS = ""综合起来,我的 fly.toml 文件如下所示:
app = "syncthing-mtlynch"
[build]
image = "syncthing/syncthing:1.23.4"
[env]
# Only listen for connections to admin GUI through localhost.
STGUIADDRESS = ""
[mounts]
source="syncthing_data"
destination="/var/syncthing"
[[services]]
internal_port = 22000
protocol = "tcp"
[[services.ports]]
port = 22000
[[services.tcp_checks]]
grace_period = "1s"
interval = "15s"
restart_limit = 0
timeout = "2s"
[[services]]
internal_port = 22000
protocol = "udp"
[[services.ports]]
port = 22000
[[services]]
internal_port = 21027
protocol = "udp"
[[services.ports]]
port = 21027不借助 Tailscale 配置 Syncthing
现在,Syncthing 已经在 Fly.io 上运行了!我可以从日志中获取设备 ID,并把我的 Fly.io Syncthing 节点添加为对等节点。
但还有一个问题。Syncthing 中的对等关系需要双方同意。要让我的 Syncthing 云服务器接受来自我设备的连接请求,我需要访问云服务器的管理控制面板。
我不想把 Syncthing 管理界面暴露给整个互联网。理论上我可以用强密码保护它,但网络层面的保护更强、更可靠。
Andrew Katz 通过把他 Fly.io 上的 Syncthing 服务器加入他的个人 Tailscale VPN 解决了这个问题。这样 Andrew 可以在 VPN 内访问管理界面,同时阻止其他任何人连接。
正如我之前提到的,每台 Fly.io 服务器都内置了 Wireguard VPN,那么我能通过这种方式访问管理界面吗?
我先尝试通过 SSH 连接到我的服务器:
$ fly ssh console
Connecting to fdaa:0:20ad:a7b:15f:92b0:4091:2... complete
32874e1dc76685:/#很简单。现在我有了 Syncthing 服务器的控制台访问权限,可以运行任何想要的命令。
使用标准的 ssh 工具,你可以把本地端口隧道转发到 SSH 连接的另一端。如果我能把本地端口 8384 隧道转发到我 Syncthing 服务器的端口 8384,就能访问它的管理控制面板了。
遗憾的是,fly ssh 命令不支持端口转发,所以这条路走不通。但我发现 fly 工具有一个 proxy 命令,于是我试了一下:
$ fly proxy 8384:8384
Proxying local port 8384 to remote [syncthing-mtlynch.internal]:8384好吧,看起来它在做点什么。但接着我尝试连接,却毫无反应:
$ curl http://localhost:8384
curl: (56) Recv failure: Connection reset by peer我尝试了一个更简单的测试:在端口 8000 上启动 netcat,然后尝试代理到那个端口:
32874e1dc76685:/# nc -l 8000$ fly proxy 8000:8000
Proxying local port 8000 to remote [syncthing-mtlynch.internal]:8000$ curl http://localhost:8000
curl: (56) Recv failure: Connection reset by peer在我的 Fly.io 服务器上,netcat 没有显示任何连接尝试。这是怎么回事?
我找到了一个 Fly.io 论坛帖子,标题是“Fly proxy seemingly doesn't work”,这个标题恰好道出了我当时的心情:
我正尝试通过 fly proxy 8080 连接我的本地电脑,它告诉我以下信息:
Proxying local port 8080 to remote [notion-to-calendar.internal]:8080然而,
curl localhost:8080或curl 0.0.0.0:8080会一直挂起,直到我关闭代理。
Fly.io 团队的 @jerome 解释了原因:
只有绑定在 ipv6 上的监听器才能通过
fly proxy命令访问。
啊,IPv6!这就解释通了。如果 Syncthing 监听的是 IPv4 接口,那它就收不到来自 Fly.io 代理的连接。
我在维护 Sia Docker 镜像时遇到过类似的问题。当时的解决方案是用一个叫 socat 的工具来代理连接,于是我在这里也试了一下:让它监听 IPv6 端口 8386,并把连接代理到 IPv4 端口 8384。
apk add socat && \
socat TCP6-LISTEN:8386,fork,su=nobody TCP4:localhost:8384然后我更新了 fly proxy 命令,让流量发往 IPv6 端口:
fly proxy 8384:8386瞧!成功了。我终于可以从本地设备访问我的 Syncthing 云服务器的管理控制面板了。
Andrew Katz 的方案有一个优势:他可以随时访问自己 Fly.io 服务器的管理界面。而如果我想对我的 Fly.io 服务器做管理操作,就得经历搭建临时代理这一套繁琐流程,但这对我来说其实无所谓。我预计维护不会太频繁,所以不介意这里有点笨拙。
能避开 socat 这种 hack 吗?
用 socat 代理 IPv6 虽然可行,但既丑陋又绕弯。有没有更干净的办法?
看起来 Syncthing 本身原生支持 IPv6,所以我试着让它监听 Fly.io 服务器的 IPv6 回环接口 ::1:
[env]
STGUIADDRESS = "[::1]:8384"我用 fly deploy 重新部署,一切正常启动。日志显示 Syncthing 现在监听在 ::1 上:
INFO: Access the GUI via the following URL: http://[::1]:8384/到目前为止一切顺利。我再试一次代理命令:
$ fly proxy 8384:8384
Proxying local port 8384 to remote [syncthing-mtlynch.internal]:8384现在,我再试着通过 8384 端口连接:
$ curl http://localhost:8384/
curl: (56) Recv failure: Connection reset by peer可恶!就差一点。
在我的 Syncthing 服务器日志中,我看到几十行这样的消息:
[UPMD6] 2023/05/25 11:41:16 INFO: Listen (BEP/tcp): TLS handshake: EOFTLS?什么情况?Syncthing 的日志明明说它在监听明文的 http:// 连接。不过出于好玩,我还是试了 HTTPS 协议:
$ curl https://localhost:8384/
curl: (35) OpenSSL SSL_connect: Connection reset by peer in connection to localhost:8384不行,还是没戏。我感觉自己离解决方案已经很近了,如果读者有什么想法,请告诉我。
我考虑过的另一条路是完全跳过 Web 图形界面,像真正的黑客一样全部通过 CLI 操作。
$ syncthing cli --home /var/syncthing/config config devices
NAME:
syncthing cli config devices -
USAGE:
syncthing cli config devices command [command options] [arguments...]
COMMANDS:
ACTIONS:
list List item keys in the collection
add Add a new item to collection
add-json Add a new item to collection deserialised from JSON但 Syncthing 的 CLI 看起来相当复杂,所以我决定 Web 图形界面对我来说已经够用了。
更新(2023-06-29):我们有解决方案了!
感谢那些告知我可以使用 fly-local-6pn 地址的读者。我可以在 fly.toml 中加入这个环境变量:
STGUIADDRESS = "fly-local-6pn:8384"之后,我就可以使用 flyctl proxy 8384:8384 并正常连接了。我已经更新了下方的教程,以利用这一改进。
如何将 Syncthing 部署到 Fly.io
经过大量反复试验,我对 Syncthing 和 Fly.io 已经摸得比较透了,现在可以给出一种干净的 Syncthing 部署方法。整个过程从头到尾大约只需要五分钟。
前提条件
开始之前,你需要:
- 一个 Fly.io 账户(已激活计费)
- 在你的机器上安装并完成认证的
flyCLI
创建你的应用
首先,创建一个新的 Fly.io 应用。
下面的代码片段会把你的应用命名为 syncthing- 加上一个随机后缀,但你也可以选择任何尚未被占用的应用名称。
RANDOM_SUFFIX="$(head /dev/urandom | tr -dc 'a-z0-9' | head -c 6 ; echo '')"
APP_NAME="syncthing-${RANDOM_SUFFIX}"
fly apps create --name "${APP_NAME}"创建 Fly.io 配置文件
接下来,为你的部署创建一个 Fly.io 配置文件。
我希望确切知道自己运行的版本,所以我把 SYNCTHING_VERSION 设置为明确的 1.23.4 版本镜像。如果你希望你的 Fly.io 服务器在每次新部署时都运行最新的稳定版本,可以把版本设置为 latest。如果你想玩点刺激的,也可以选择 edge 或 nightly,获得带有最前沿特性的不稳定版本。
对于 REGION,请选择一个离你较近的 Fly.io 区域。
VOLUME_NAME 无关紧要,你可以随意更改。
SYNCTHING_VERSION="1.23.4"
REGION="ewr" # Deploy to Fly.io's Newark, NJ, USA data center.
VOLUME_NAME="syncthing_data"
cat <<EOF > fly.toml
app = "${APP_NAME}"
primary_region = "${REGION}"
[build]
image = "syncthing/syncthing:${SYNCTHING_VERSION}"
[env]
# Only listen for connections to admin GUI through fly.io's private Wireguard
# network.
STGUIADDRESS = "fly-local-6pn:8384"
[mounts]
source="${VOLUME_NAME}"
destination="/var/syncthing"
[[services]]
internal_port = 22000
protocol = "tcp"
[[services.ports]]
port = 22000
[[services.tcp_checks]]
grace_period = "1s"
interval = "15s"
restart_limit = 0
timeout = "2s"
[[services]]
internal_port = 22000
protocol = "udp"
[[services.ports]]
port = 22000
[[services]]
internal_port = 21027
protocol = "udp"
[[services.ports]]
port = 21027
EOF创建持久卷
你需要一个持久卷,这样 Syncthing 就不会在每次服务器重启时丢失你的配置和数据。
卷的大小可以任选,但截至撰写本文时,Fly.io 在其免费套餐中提供 3 GB。
SIZE_IN_GB="3" # This is the limit of fly.io's free tier as of 2023-05-24
fly volumes create "${VOLUME_NAME}" \
--region "${REGION}" \
--size "${SIZE_IN_GB}" \
--yes部署你的服务器
最后,是时候部署你的应用了。Syncthing 没有必要购买 IPv4 地址,所以你可以加上 --no-public-ips 标志:
fly deploy --no-public-ips如果一切正常,你应该会看到类似这样的消息:
No machines in group app, launching a new machine
Machine e286537dbd3586 [app] update finished: success
Finished launching new machines
Updating existing machines in 'syncthing-ccdb2x' with rolling strategy
Finished deploying将你的 Syncthing 云服务器添加为对等节点
一旦你的 Syncthing 服务器运行起来,你就需要它的设备 ID 才能连接。你可以在服务器日志中找到它:
$ fly logs | grep "My ID: "
2023-05-26T04:20:28Z app[e784e736c90283] ewr [info][GHLLB] 2023/05/26 04:20:28 INFO: My ID: GHLLBWT-QJ4LGHJ-RT43QUV-DWFRMGS-5OTXHGH-LAZAIMG-HQ3TVAE-UUC2SA5在你本地的 Syncthing 设备上,点击“Add Device”并输入 Device ID 来添加你的云 Syncthing 服务器。你可以给服务器起任意设备名。我选了一个极具创意的名字:cloud-syncthing。
你可以把云 Syncthing 服务器标记为“Untrusted”(不受信任),以提高安全性。这会告诉你的其他设备先加密数据再发送给服务器。即使攻击者攻陷了你的 Fly.io 服务器,他们也只能拿到不可读的加密数据。
最后,把你其中一个文件夹共享给新的 Syncthing 服务器。前往 Edit Folder > Sharing,勾选新对等节点的复选框。如果你已将其标记为不受信任,请设置一个强密码短语来加密数据。
访问 Web UI
要访问你的 Fly.io 服务器的 Syncthing 管理控制面板,请打开一个代理,把本地端口 8388 连接到你的 Fly.io 服务器的端口 8384:
fly proxy 8388:8384代理建立后,你应该可以通过 localhost 地址从本地设备访问云服务器的 Syncthing 控制面板:
你应该会看到类似如下的管理控制面板:
恭喜!你已经把 Syncthing 服务器部署到了云端,并且现在拥有对它的完全访问权限。从这里开始,你可以像配置其他任何运行 Syncthing 的设备一样对其进行配置。
当你在 Fly.io 服务器上完成 Syncthing 设置的配置后,用 Ctrl+C 终止 fly proxy 命令即可。
随机一篇博客







