在 Fly.io 雲端伺服器上部署 Syncthing
我最近發現了 Syncthing,這是一款可在多台機器間同步檔案的開源工具。
在個人裝置上設定 Syncthing 很容易,但將它部署到雲端伺服器的過程卻是一段有趣的旅程。
為什麼要在雲端執行 Syncthing?
Syncthing 以點對點(peer to peer)的方式同步檔案。這表示至少要有兩台裝置同時上線並執行 Syncthing,才能保持同步。如果我在桌機上修改檔案後關機,再帶著筆電出差,筆電就無法取得我在桌機上所做的變更。
如果有一台始終在線且可用的雲端伺服器執行 Syncthing,就能防止裝置之間失去同步。
我不想聽你的落落長故事——直接告訴我怎麼部署 Syncthing 就好
我會分享幾種部署 Syncthing 失敗的做法。如果你想直接跳到解法,請參閱〈「如何將 Syncthing 部署到 Fly.io」〉一節。
先前成果:Syncthing + Tailscale 在 Fly.io 上的應用
過去兩年來,Fly.io 一直是我首選的雲端代管服務商,因此我搜尋了是否有人寫過在 Fly.io 上使用 Syncthing 的文章。結果發現 Andrew Katz(安德魯·卡茲)不到一年前就寫過一篇很棒的教學。
安德魯·卡茲的教學是個好消息,因為它證明了我的想法是可行的。我唯一有點意見的是,它依賴 Tailscale 這個熱門的 VPN 解決方案。我很喜歡 Tailscale,但在這個情境下它有一些嚴重的缺點。
將 Syncthing 與 Tailscale 結合需要建置自訂的 Docker 映像檔。隨著兩個工具不斷演進,更新該映像檔會帶來不小的維護負擔。而且在單一容器中混合兩個應用程式 有點違反 Docker 的禁忌。
所有 Fly.io 伺服器預設都內建 Wireguard VPN。我猜想,與其混入 Tailscale,不如直接利用現有的 Wireguard 連線來改良安德魯·卡茲的解法。
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 原始碼,他們的執行階段映像檔 depends on 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 伺服器以允許任何連入流量。
如果放著不管夠久,它可能會透過 中繼伺服器連上,但如果正確設定連入連接埠,Syncthing 的運作會更好。
為 Syncthing 設定防火牆連接埠
此時,Syncthing 已在 Fly.io 上啟動並執行,但無法接受來自其他裝置的流量。
Syncthing 提供了非常清楚的文件,說明如何設定防火牆以開放 與對等裝置通訊所需的連接埠:
Port 22000/TCP:基於 TCP 的同步協定流量
Port 22000/UDP:基於 QUIC 的同步協定流量
Port 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 設定為僅在 loopback 介面上監聽。
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 的管理介面暴露給整個網際網路。理論上我可以用一組高強度密碼來保護它,但網路層級的防護更強大、也更可靠。
安德魯·卡茲 解決了這個問題,他將自己的 Fly.io Syncthing 伺服器加入個人 Tailscale VPN。這讓安德魯·卡茲能在 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,就能存取 Syncthing 伺服器的管理儀表板。
可惜的是,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 proxy seemingly doesn't work」 的 Fly.io 論壇貼文,完全說中了我當時的心情:
我正嘗試透過 fly proxy 8080 連接我的本地電腦,它顯示了以下訊息:
Proxying local port 8080 to remote [notion-to-calendar.internal]:8080然而,
curl localhost:8080或curl 0.0.0.0:8080就只是卡住,直到我關閉 proxy。
來自 Fly.io 團隊的 @jerome 解釋了原因:
只有綁定在 ipv6 上的監聽器才能透過
fly proxy指令存取。
啊,IPv6!這就說得通了。如果 Syncthing 監聽在 IPv4 介面上,就不會收到來自 Fly.io proxy 的連線。
我在 維護 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 雲端伺服器的管理儀表板。
安德魯·卡茲的解法優勢在於讓他的 Fly.io 伺服器管理介面隨時可用。如果我想對 Fly.io 伺服器進行管理上的變更,就得經歷一番設定臨時代理的繁瑣步驟,但對我來說其實沒關係。我預期維護不會很頻繁,所以有點笨拙也無妨。
可以不用 socat 這個權宜之計嗎?
透過 socat 代理 IPv6 雖然可行,但既醜陋又迂迴。有沒有更乾淨的做法呢?
Syncthing 似乎原生就支援 IPv6,所以我嘗試讓它監聽 Fly.io 伺服器的 IPv6 loopback 介面 ::1:
[env]
STGUIADDRESS = "[::1]:8384"我用 fly deploy 重新部署,一切都正常啟動。日誌顯示 Syncthing 現在正在 ::1 上監聽:
INFO: Access the GUI via the following URL: http://[::1]:8384/目前看來一切順利。我再試一次 proxy 指令:
$ 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還是不行。我覺得自己已經很接近解法了,所以如果讀者有想法,歡迎告訴我。
我考慮過的另一條路是完全跳過網頁 GUI,像個真正的駭客一樣全部透過 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 似乎相當複雜,所以我決定網頁 GUI 對我來說就夠用了。
更新(2023-06-29):我們找到解法了!
感謝讀者告知可以使用 fly-local-6pn 位址。我可以用這個環境變數來更新我的 fly.toml:
STGUIADDRESS = "fly-local-6pn:8384"從此之後,我就可以使用 flyctl proxy 8384:8384 並像平常一樣連線。我已更新下方的教學,以採用這項改良。
如何將 Syncthing 部署到 Fly.io
經過大量試錯摸索 Syncthing 與 Fly.io 之後,我準備好要呈現一個乾淨俐落的部署方式。從頭到尾應該只需要大約五分鐘。
前置需求
開始之前,你需要:
- 一個 Fly.io 帳號(已啟用帳單)
- 已在機器上 安裝並完成驗證的
flyCLI
建立你的應用程式
首先,建立一個新的 Fly.io 應用程式。
下方的程式碼片段會將你的應用程式命名為 syncthing- 加上隨機後綴,但你也可以選擇任何在 Fly.io 上尚未被使用的應用程式名稱。
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 並勾選新對等裝置的核取方塊。如果你將其標記為不受信任,請設定一組高強度的通行密語來加密資料。
存取網頁介面
要存取 Fly.io 伺服器上的 Syncthing 管理儀表板,請開啟代理,將本地連接埠 8388 連接到 Fly.io 伺服器的連接埠 8384:
fly proxy 8388:8384代理建立後,你應該就能從本地裝置透過 localhost 網址存取雲端伺服器的 Syncthing 儀表板:
你應該會看到如下的管理儀表板:
恭喜!你已將 Syncthing 伺服器部署到雲端,現在可以完整存取它。從這裡開始,你可以像設定其他任何執行 Syncthing 的裝置一樣來設定它。
在 Fly.io 伺服器上完成 Syncthing 設定後,請以 Ctrl+C 終止 fly proxy 指令。
隨機一篇部落格







