Bluesky 從 2024 年打開聯邦大門到 2026 年年中,第三方 PDS 數量從個位數成長到將近 2800 臺。這個數字看起來不大,比對整個 ATProto 網路超過三千萬使用者的規模,第三方自架 PDS 承載的帳號量還是零頭。但這個生態最有意思的地方不在數量,而在門檻已經低到一個週末就能架起來,只要願意繞開幾個 Bluesky 官方預設就會踩到的坑。
自架 PDS 的意義跟自架 Mastodon 不太一樣。Mastodon 的 instance 是社群單位,選擇 instance 意味著選擇一群人跟一套審核規則。ATProto 的 PDS 只是儲存節點,Feed 演算法、審核標記、貼文串接全部在網路層另外的服務處理。這代表自架 PDS 主要買到的不是社群自主,而是資料所有權:DID 綁在自己控制的域名下,貼文的原始資料存在自己的伺服器上,Bluesky 公司要是哪天決定改收費、被收購、關站,帳號還在。
官方 installer 幫倒忙的地方
Bluesky 提供的 installer.sh 走的路線是「一鍵完成」。腳本會裝 Docker、跑 Caddy container 在 host network、佔用 80 跟 443 port、寫一份 docker-compose.yml、生成環境變數檔。對什麼都沒有的乾淨 VPS 來說,這確實方便;但只要那臺 VPS 上已經跑了 nginx、Traefik,或任何佔用 443 port 的服務,Caddy 就起不來,整個 installer 也會失敗,且錯誤訊息不夠明確。
社群長期抱怨的另一個點是這個腳本假設所有人都想要 Docker、想要 Caddy、想要走 Let’s Encrypt。既有的反向代理配置完全沒進入考量範圍,也沒有跳過憑證管理的選項。實務上要嘛把 installer 拉起的所有容器砍掉、自己寫一份 compose 檔,要嘛把 PDS 隔離到一個乾淨的子域名跟獨立 Docker 網路裡,跟現有服務物理隔開。
真正卡人的坑是上傳限制。PDS_BLOB_UPLOAD_LIMIT 這個環境變數看起來像是控制上傳大小的關鍵,但當反向代理是 nginx 時,實際上限會被 client_max_body_size 這個 nginx 預設的 1MB 卡死。使用者遇到 413 錯誤時,第一個直覺是調 PDS 環境變數,怎麼調都沒用。解法是在 nginx 端加 client_max_body_size 0;,才能讓 PDS 端的限制真的生效。這種雙重限制的除錯路徑,官方文件完全沒提。
硬體門檻比想像的低,頻寬另一回事
官方文件標的最低配置是 1 vCPU、1GB RAM、20GB SSD。這個數字看起來像是行銷用語,但實測下來確實成立。單使用者的 PDS 常駐記憶體佔用大約 300MB,SQLite 資料庫在使用一年後大概 500MB 到 1GB,blob 目錄視上傳圖片影片而定。用最基本的 VPS 方案跑到二十個帳號共享,資源上仍然綽綽有餘。
真正吃資源的不是計算,是頻寬。PDS 對外的流量分成兩塊:一是使用者跟自己 PDS 的直接互動,量不大;二是 Relay 從自己 PDS 拉取 firehose 資料流,這個流量會隨著發文頻率累積。加上 ATProto 使用 CBOR 格式編碼,每筆記錄的 overhead 比 JSON 略高。單使用者的 PDS 每月流量大約在幾 GB 到十幾 GB 之間,這是多數 VPS 方案都不會超額的範圍。
Handle 驗證的兩條路,DNS 那條比較不痛
ATProto 的 handle 綁定機制設計得很嚴謹。每個 handle 都要能被驗證為屬於某個 DID,驗證方法有兩種:DNS TXT record,或是 HTTPS well-known endpoint。DNS 那條路實務上比較穩,只要在 DNS 端加一筆 _atproto.example.com 的 TXT record、內容填上 did=did:plc:xxxxx,驗證就過。
HTTPS 那條路是在 https://example.com/.well-known/atproto-did 放一個純文字 DID。這個做法在 root 域名下沒問題,但如果 PDS 綁的是子域名,會遇到跨域驗證的怪問題。有部落格作者記錄過一個案例:他的 PDS 掛在 pds.example.com 底下,結果 handle 顯示成 @user.pds.example.com,DNS 驗證跟 HTTPS 驗證都不接受這個結構。最後只能在 nginx 端硬編 handle 跟 DID 的對照表,這種做法他自己都承認撐不住多使用者部署。
繞過方法是一開始就想清楚 handle 的命名規則。要嘛把 PDS 直接架在 root 域名,要嘛用完全獨立的短域名,不要用「子域名下的多層 handle」這種結構。取名這一步做對,後面的事情會少一半。
Cocoon 跟 Tranquil 分別解決什麼
Bluesky 官方 PDS 用 TypeScript 寫、跑在 Node.js runtime 上,是 ATProto 目前的 reference implementation。三個社群 PDS 實作在 2026 年逐漸成熟:Cocoon 用 Go 寫、預設 SQLite、可選 PostgreSQL 跟 S3 相容儲存;Tranquil 用 Rust 寫、強調規格遵從跟功能完整度;millipds 用 Python 寫、定位是研究性質。
Cocoon 的設計目標很明確:把資源佔用壓到最低。作者本人已經把 Bluesky 主帳號搬過去跑了一段時間,SQLite 加上內建每小時 S3 備份的組合對單使用者剛好,Go binary 直接執行檔的部署方式也比 Node.js 加 npm 依賴乾淨。缺點是文件不夠齊、實作還在快速迭代,出問題時很可能只能靠讀原始碼。
Tranquil 的定位是給認真想跑多使用者 PDS 的人。它把 outbox 事件保留時間降到預設 24 小時,這個決定看起來像限制,實際上是刻意的:ATProto 規格允許 PDS 有自己的保留策略,不是每個 PDS 都需要當永久儲存節點。對只需要維持自己資料所有權的自架者,24 小時的 outbox 加上 SQLite 保存實際 record 就已經夠用。
對只跑自己一個帳號的使用者,官方 PDS 或 Cocoon 都是合理選擇。Tranquil 目前的成熟度還不到「無腦選它」的地步,但如果未來要開放註冊給朋友、家人,或跑一個十人以內的小型 PDS,值得留意它接下來一年的演進。
帳號從 bsky.social 搬過來,DID 帶著走
ATProto 支援帳號在 PDS 之間遷移,是這個協定跟 ActivityPub 相比很不一樣的一個地方。整個流程走完之後,DID 保持不變、追蹤關係全部帶著走、既有的貼文串接不會斷。使用者從其他人的視角看就像什麼都沒發生,只是這個帳號的資料儲存位置換了。
實作上分四步:在新 PDS 建立占位帳號、把舊 PDS 的 repository 匯出成 CAR 檔、透過 goat 或 psadmin 工具把 CAR 匯入新 PDS、更新 DID 上的 PDS 端點紀錄。這個流程唯一比較麻煩的是需要協調兩邊的 admin 權限跟時間點,但對只搬自己一個帳號的場景,半小時可以完成。
DID 這一層是整個設計的精髓。DID 綁定的是 handle 跟公開金鑰,不是 PDS 地址;PDS 只是 DID document 裡註記的目前儲存位置。這代表帳號的長期身份被 DID 而不是 PDS 決定,這也是為什麼自架 PDS 沒有「burn the boats」的風險——想搬回 bsky.social 隨時可以搬回去,遷移是雙向可逆的操作。
架在臺灣 VPS 的實際考量
Bluesky 官方 Relay 目前部署在美國,PDS 跟 Relay 的通訊延遲會直接影響 firehose 資料流的即時性。從臺灣機房到美西的延遲大約 130 到 180 毫秒,這個數字對 PDS 本身的運作沒有問題,但如果之後想跑自己的 Relay,或者想把 PDS 拿來承載即時互動比較密集的應用,延遲就會是需要納入考慮的因素。
反過來看,臺灣 VPS 對本地使用者的存取速度是最直接的優勢。ATProto 的 blob 存取(圖片、影片、頭像)走的是 PDS 直接吐內容的路徑,不是全部走 CDN。這代表如果 PDS 上的帳號主要被臺灣讀者追蹤,圖片載入速度會直接反映到 PDS 所在機房的頻寬跟就近性。
跑 PDS 需要的實際條件比大部分自架服務簡單:一顆 IP、可控的 DNS、支援 IPv4 outbound(跟 Relay 通訊需要)、開放 80/443 給外部連入。NCSE Network 在臺灣是方電訊機房提供搭載 Intel Gold CPU 與 NVMe SSD 的 VPS 主機,1 vCPU 方案跑單使用者 PDS 綽綽有餘,將來要橫向擴充到多使用者或加開自架 Jetstream 節點,往上調規格也不會有斷點。對想在聯邦網路上留一個自己控制的節點的臺灣使用者,這是把資料所有權真正拿回來的一步。