你照 Cloudflare 那條路線架好了 EdgeEver:fork 一份 repo,Workers Builds 綁在你 fork 的 main 上。過一陣子打開自己的 fork,GitHub 在分支上方提示你落後 upstream,旁邊就是那顆 Sync fork。你按不按?

2026 年 8 月 13 日,答案是按。專案開了一張 #234,標題寫著「[重要] 现有用户请手动执行一次 Sync fork」。

現在去讀同一條路線的部署文件 cloudflare-workers-builds.md,答案反過來了:

Prefer this workflow over GitHub Sync fork. Sync fork follows upstream main history and can make the next stable run a deliberate no-op.

README 講得更直接:如果你的 fork 也拿來開發,「do not develop on or Sync fork the deployment main」。

同一顆按鈕,8 月 13 日的 issue 叫你按,10 月 1 日讀到的現行文件叫你別按。照舊教學操作的人,會做出跟現行文件相反的事。

這篇整理的都是文件。我沒有安裝 EdgeEver,沒走過 Cloudflare 或 Docker 任何一條部署,也沒跑過它的 updater。下面的東西來自 repo 裡的 README、docs/ 底下的部署文件、Dockerfile,加上 2026-10-01 用 gh api 查的 release 與 issue 資料。

後端只有一份,所以問題不在後端

EdgeEver 是一個筆記服務,repo 在 tianma-if/edgeever,授權 AGPL-3.0。2026-10-01 查的時候,最新版是 v1.95.0,1,919 顆星、997 個 fork。repo 建立於 2026 年 6 月 26 日,到今天三個月出頭。

它最常被拿出來講的設計,是同一個後端跑兩個地方。self-hosting-architecture.md 的原句:

Cloudflare and Docker execute the same Hono application and differ only at thin runtime and infrastructure adapter seams.

做法也寫得很清楚。業務邏輯只認 apps/api/src/storage-contract.ts 裡的兩個介面:DatabaseAdapter 管 SQL statement 與 batch,BlobStoreAdapter 管附件的 get、put、delete。Cloudflare 那邊把 D1 和 R2 的 adapter 注入 fetchEdgeEverApp,Bun 的進入點把 SQLite 加檔案系統、或 SQLite 加 S3 的 adapter 注入同一個函式。PostgreSQL 的介面先定了名字,文件直接寫「It is intentionally not implemented yet.」

同一份文件還訂了兩條不准動的線:migrations 只能往後加,「do not fork the schema for Docker」;/api/*、/mcp、/api/health、/api/openapi.json 這些路徑不動。

這一段讀起來很舒服,也是我覺得最不用擔心的部分。插座規格訂死了,牆上插的是 D1 還是 SQLite 都一樣用。

「一份程式碼、兩個環境」真正難的地方在升級,儲存層反而是最好處理的一塊。程式碼可以共用,升級的路沒辦法共用。Cloudflare 的使用者手上是一個 GitHub fork,Docker 的使用者手上是一台主機加一個 volume,兩邊能下手的地方完全不同,所以專案在兩邊各長出了一套規矩。

Cloudflare 那條:從「你來按」變成「別碰 main」

Cloudflare 路線的現行做法是這樣串的:你 fork repo,Cloudflare Workers Builds 綁住 fork 的 main,再由一支叫「Update deployed EdgeEver」的 GitHub Actions workflow,把你的 fork 當成 upstream 的部署鏡像來更新。預設 channel 是 stable,追最新的正式 Release tag;在 repository variable 設 EDGE_EVER_UPDATE_CHANNEL=edge,就改追 upstream 的 main。

這樣看,文件反對 Sync fork 就不奇怪了。照文件的描述,沒改過程式碼的 fork,更新方式是把產品快照套成一個新的線性 commit(後面會講),Sync fork 拉進來的卻是 upstream main 的整段歷史。文件只說這可能讓下一次 stable 執行變成刻意的 no-op,沒展開內部怎麼判斷。我的讀法是:你訂的是月刊,卻自己跑去印刷廠抄了一份最新草稿回家,下一期月刊來的時候,系統看你手上已經有東西,就不寄了。這套機制是我的推測,文件沒有這樣寫。

那 8 月 13 日為什麼要大家按?因為那時候自動更新真的卡住過。v1.21.4 的 release note(2026-08-13 發布)是這樣寫的:

Cloudflare deployment Forks now preserve their local updater layer when applying product releases, so official packaging and signing workflow changes no longer block automatic updates. Existing Fork deployments only need to complete the one-time GitHub Sync fork described in #234

壞掉的是「官方改了打包與簽章的 workflow,fork 的自動更新就被卡住」,對應的 issue 是 #240「Prevent official workflows from blocking Cloudflare Fork updates」。當時的補法分兩步:程式上讓 fork 保留自己那層 updater,使用者端則請已經在跑的人手動 Sync fork 一次。到了現行文件,Sync fork 已經不是日常動作,日常更新改成優先走 workflow。

按鈕也沒有完全退休。現行文件還留著一個例外:舊版 updater 推送時如果被拒、訊息是 without workflows permission,就「use GitHub Sync fork once … then re-run」。所以讀 EdgeEver 的 Cloudflare 教學,看到 Sync fork 這個字,要先分清楚它在講日常更新,還是在講這個救援情境。

這條路線另一個我很喜歡的地方是失敗時的處理。如果你的 fork 有改過程式碼,設了 EDGE_EVER_PRESERVE_FORK_CHANGES=true,合併 release 時會先跑 local migrations、完整的非 E2E 測試、type check 和 production build,文件原句是「any failure leaves main and production unchanged」。沒改過程式碼的唯讀 fork 走另一條,「without installing dependencies or running the project test suite」,直接把產品快照套成一個新的線性 commit。

Docker 那條:預設幫你更新,後面再叫你別用預設

Docker 路線的起點是一行安裝指令:

1
curl -fsSL https://edgeever.org/install.sh | bash

照 deploy-docker.md 的說法,安裝器預設會在目前使用者的 crontab 裡排一筆,每天 server time 04:17 跑一次 ~/edgeever/update.sh,拉的是 latest 標籤。安裝時用 --version 指定版本的話,「a version supplied with –version remains pinned」;不想自動更新,加 --no-auto-update 或設 EDGE_EVER_AUTO_UPDATE=false。

要持久化的東西只有一個目錄 /data,裡面是 edgeever.sqlite、edgeever-secrets.json 和 resources。容器啟動時,會先套用跟 D1 那邊同一套 append-only 的 migrations/*.sql。

同一份文件往下捲到「Upgrade and rollback」,語氣就變了:

Use immutable release tags instead of latest for production

緊接著還有一句:

Application rollback does not reverse a database migration; restore the pre-upgrade volume backup when a data rollback is required.

拿這兩句對照安裝器的預設,你一行指令裝出來的東西,是每天凌晨自動追 latest 的實例;文件後半段卻告訴你,production 不要用 latest,退版也退不回資料庫。預設值和建議值寫在同一份文件裡,方向相反。

這件事的分量,跟發版速度有關。v1.93.0 在 2026-09-30 05:20(UTC)發布,v1.94.0 在同一天 14:55,v1.95.0 在 10 月 1 日 05:13,不到 24 小時三個版本。以這個節奏推下去,每天 04:17 那一次更新,很可能每天都會換到新版本,每一次都可能帶 migration。這是我從發版時間推的,不是文件講的。

失敗保護也不對稱。Cloudflare 那邊寫明了測試失敗就不動 production;Docker 這邊,我在文件裡只讀到 updater 會檢查容器的 health,沒有看到同等的描述。

兩條路線並排對照:

Cloudflare 路線 Docker 路線
更新由誰觸發 GitHub Actions workflow「Update deployed EdgeEver」 安裝器寫進 crontab,每天 04:17(server time)跑 ~/edgeever/update.sh
預設追什麼 stable:最新的正式 Release tag latest 標籤
改變預設 EDGE_EVER_UPDATE_CHANNEL=edge 改追 upstream main --version 釘版本;--no-auto-update 或 EDGE_EVER_AUTO_UPDATE=false 關閉
文件叫你避開的動作 日常用 Sync fork(without workflows permission 時例外) production 用 latest
更新失敗時 設了 EDGE_EVER_PRESERVE_FORK_CHANGES=true 的 fork:任一檢查失敗,main 與 production 不變 文件寫 updater 會檢查容器 health,未見同等描述

舊的啟動指令也要活過升級

Docker 這條還有一個小地方,很能看出這個專案怎麼想「升級」。

v1.62.0 的 release note(2026-09-08)寫:「Docker self-hosting starts more reliably across supported Linux environments and now packages all runtime dependencies correctly.」現在 main 上的 Dockerfile,啟動指令是:

1
CMD ["bun", "scripts/self-hosted-server.js"]

這個 .js 是 build 階段用 bun build 從 scripts/self-hosted-server.mjs 打包出來的。映像裡實際跑的已經是打包後的 .js,Dockerfile 卻還多建了一個 symlink:ln -s self-hosted-server.js /app/scripts/self-hosted-server.mjs。

Dockerfile 的註解說得很明白:NAS 或 GUI 面板升級時,常常會沿用 v1.62 時期存下來的啟動指令 bun scripts/self-hosted-server.mjs。架構文件也寫「keep scripts/self-hosted-server.mjs as an alias for NAS/GUI command overrides saved from v1.62 and earlier」。

映像換了,使用者存在面板裡的那行舊指令沒換,所以專案留一個同名的捷徑給它。這種照顧,光讀上面那句 v1.62.0 的 release note 看不出來,要讀到 Dockerfile 才知道。這段我同樣只讀了 Dockerfile、package.json 和文件,沒有實際 build 過映像。

同一份 README 裡的另一組對照

更新之外,README 自己也有兩句放在一起會打架的話。引言寫「Serverless & 100% Free Forever … EdgeEver can run within Cloudflare’s free quotas」,後面講到 R2 的地方寫「you must first activate an R2 subscription and add a payment method」。免費額度內也許真的不用付錢,但要綁付款方式這件事,要讀到 README 後段才會知道。#436「可以不用R2进行部署吗?绑定第三方存储桶」是 9 月 25 日(UTC)開的,到今天還是 open。

README 裡的容量數字也一樣要帶著條件看:Cloudflare 免費額度下「roughly 150,000 short notes and 50,000 images」,Docker 路線「scales on demand to easily support millions of notes」。兩個都是作者的估算,我沒在 repo 裡看到驗證資料。

如果是我要架

兩條路線其實在改同一件事:更新不再是你偶爾按一下的按鈕,變成一條專案替你維護的管線。差在手該放哪裡。Cloudflare 那邊,文件要你把手從 main 上拿開;Docker 那邊預設全自動,要上 production,得你自己把手放回去。

自己用的話,我會選 Docker,因為要備份的只有 /data 一個目錄,這點很難得。但我不會照安裝器的預設走:裝的時候就用 --version 釘住,關掉自動更新,每次要升級先把 /data 整個備份一份,再手動換版本。理由就是文件自己那兩句,production 不要用 latest,退版退不回 migration。以現在一天可以發三版的速度,凌晨自動追 latest 等於把每一次 schema 變更都交給運氣。04:17 那筆 crontab,我會一直關到 Docker 的 updater 文件也寫出 Cloudflare 那種保證為止:先跑 migration 和測試、失敗就保持現有容器不動,或者更新前會自動做 volume 備份。有其中一項,個人實例我就願意讓它每天自動追版。

走 Cloudflare 的話,我會照現行文件做,不碰那顆 Sync fork,除非看到 without workflows permission 那個錯誤。比較麻煩的是 8 月中留下來的 #234,它講的跟現行文件正好相反。照任何一份 EdgeEver 教學操作之前,先看它寫於哪一天。

參考來源