Dagger 1.0 beta 的釋出博文1第一句話是:
Dagger is third-generation CI, after Jenkins and GitHub Actions.
往下讀,“原生 GitHub 整合”一節還有一句更直接的:在倉庫裡執行 dagger cloud checks on,“This replaces GitHub Actions for your checks”,不再需要 CI 工作流檔案。
可我們以前見到 Dagger,大多是在 GitHub Actions 的某個 step 裡:run: dagger call ...。它到底是和 GitHub Actions 同一類的東西,還是跑在 GitHub Actions 裡面的一個工具?“第三代”比第二代多了什麼?博文說現在的 CI 到處是浪費,這些浪費在一個普通專案上看得到嗎?
本文用同一個 Go 小專案,在 GitHub 的同一種機器(ubuntu-latest)上,把原生 GitHub Actions 和 Dagger 各跑一遍,順著跑的過程中遇到的問題一層層往下看。
一、兩份配置各自描述了什麼
實驗專案
先交代實驗物件。shortlink 是一個短連結服務,三個包:
slug:把數字 ID 編成短字串,三個純函式測試;store:把連結存進 Postgres,一個整合測試TestPutGet,要一個真實的資料庫,地址從環境變數DATABASE_URL讀;cmd/server:HTTP 入口,沒有測試。
CI 要做的事情也很普通:golangci-lint 檢查一遍,再把測試跑一遍,測試要有 Postgres。
兩邊寫出來的樣子
先看最直觀的差別:同樣的事,兩邊各寫了什麼。
GitHub Actions 的單位是 job 和 step。 一個 workflow 由若干 job 組成,每個 job 被分到一臺 runner 機器上,按順序執行 step;step 要麼是一條 shell 命令(run:),要麼是別人寫好的一個 action(uses:,實際上是一段 Node.js 程式)。go test ./... 裡有幾個包、幾個測試,workflow 不知道,它只看得到這個 step 的退出碼。
Dagger 的單位是模組和檢查。 dagger.toml 裡沒有一行命令,只宣告裝了哪幾個模組。官方的 Go 模組2會自己找 go.mod,把每個包、每個測試函式都變成可以單獨選中的“檢查”;golangci-lint 模組看到 .golangci.yml 才會出現。安裝完以後列一下3:
$ dagger check -l --allMODULE GOLANGCI-LINT-PROJECT GO-PACKAGE GO-TEST CHECKgolangci-lint . lintgo slug TestDecodeRejectsAmbiguous testgo slug TestEncodeZero testgo slug TestRoundTrip testgo store TestPutGet test…(還有幾行是檢查 go generate 產物有沒有過期的 stale,這裡省略。)
Postgres 的寫法差別也在圖上。GitHub Actions 裡它是 job 級別的“服務容器”,和 step 並列;Dagger 裡沒有現成的資料庫模組,我們寫了一個十來行的 Go 模組 pgbase,返回一個繫結了 Postgres 服務的 Go 容器,再用 dag://pgbase/base 這個地址4把它塞給 Go 模組當測試容器。
這裡有一個相同點值得先記住:兩邊的配置都放在倉庫裡,都會最終變成在 Linux 上跑的容器和程序。 差別在於,GitHub Actions 的配置描述“怎麼跑”,Dagger 的配置描述“有什麼”,怎麼跑交給模組和引擎去算。
那麼是誰在“算”?這些東西最後跑在哪臺機器上?
二、執行和快取所在的位置
GitHub Actions:一臺用完即棄的虛擬機器
在 GitHub Actions 裡,push 事件到達後,GitHub 給每個 job 分一臺全新的虛擬機器。我們這次用的 ubuntu-latest 是 2 核、72 GB 系統盤,預裝了幾十種語言的工具鏈。step 直接在這臺機器上跑,服務容器由 runner 用 Docker 起在旁邊。job 一結束,機器就回收了,上面的一切都不留下。
想在兩次執行之間留點東西,只能顯式地打包上傳:actions/cache(setup-go 內建呼叫它)把目錄打成 tar 包存到 GitHub 的快取服務裡,下次按 key 下載解開5。
Dagger:一個引擎,放在哪都行
Dagger 的執行者是引擎(Dagger Engine),一個開源的守護程序,通常以容器形式執行。dagger 命令列只是客戶端:把工作區裡需要的檔案傳給引擎,告訴它要哪個檢查的結果。引擎把檢查展開成一張由容器操作組成的圖(拉映象、掛目錄、執行命令……),每個節點按輸入算出摘要,結果存在引擎自己的磁碟上(/var/lib/dagger)。1.0 之前這張圖的求解交給 BuildKit,從 v0.21 起換成了自己的引擎6;1.0 裡官方模組本身也改用 Dagger 自己的指令碼語言 Dang 來寫7。
引擎可以放在筆記本上、自己的伺服器上、Kubernetes 裡,也可以放在 GitHub Actions 的 runner 裡;1.0 還提供 Dagger 託管的 Cloud Engines。
這張圖回答了開頭的疑問。GitHub Actions 是一整套東西:事件觸發、分配機器、按步驟執行、存快取。 Dagger 引擎只佔其中的“執行”和“快取”兩層。最常見的用法(中間一列)是把引擎塞進 GitHub Actions 的一個 step 裡,兩者是巢狀關係,不是替代關係。博文說的“replaces GitHub Actions”,指的是右邊一列:Dagger Cloud 直接接收 GitHub 的事件,在自己的引擎上跑,再把結果寫回 GitHub 的 check status。
中間那個虛線框是後面所有問題的伏筆:引擎的快取存在 runner 的磁碟上,而 runner 用完就扔。 這對快取意味著什麼?
三、快取:誰知道什麼變了
博文列舉的第二種浪費是“shell 指令碼不知道什麼變了,所以機器一遍遍重算同樣的東西”。Solomon Hykes 在更早的一篇裡說得更直白:CI 的瓶頸是軟體問題,不是機器不夠快8。我們用四個場景來看兩邊各重算了什麼:
- 冷啟動:什麼快取都沒有;
- 原樣再跑一遍;
- 給
slug/slug.go末尾加一行註釋; - 只改
README.md。
原生 GitHub Actions 每個場景是一次單獨的 push(每次一臺新機器,快取靠 actions/cache 傳)。Dagger 分兩種:在同一臺 runner 上連續執行 dagger check(引擎一直開著,相當於開發者的筆記本),以及每次 push 在新 runner 上從頭執行。
| 場景 | 原生 GitHub Actions (每次新機器,job 全程) | Dagger,同一臺機器 (dagger check) | Dagger,每次新機器 (dagger check) |
|---|---|---|---|
| 冷啟動 | 56 s lint 31 s,測試 4 s | 159 s | 159–167 s 每次都是冷啟動 |
| 原樣再跑 | 27 s 測試 (cached) | 8.9 s 全部 CACHED | |
| slug.go 加一行註釋 | 39 s 測試 (cached) | 11.2 s 測試操作重跑 | |
| 只改 README.md | 未單獨測;workflow 不按路徑過濾時整個 job 重跑 | 8.7 s 全部 CACHED | — |
ubuntu-latest(2 核),2026 年 10 月 25 日,Dagger v1.0.0-beta.21。Dagger 列只計 dagger check 這條命令;新機器上每次還要先載入模組,加上騰磁碟空間,整個 job 6 分半到 7 分鐘。
原生 GitHub Actions:快取只是一個 tar 包
GitHub Actions 自己並不判斷“什麼變了”。setup-go 做的事情是:用 go.sum 的雜湊拼一個 key,命中就把上次存下的 ~/.cache/go-build 和 ~/go/pkg/mod(這次 23 MB)解壓回來9。真正判斷哪些包要重編、哪些測試要重跑的,是 Go 工具鏈自己的構建快取。所以第二輪以後測試那一步只要一兩秒,日誌裡是:
ok example.com/shortlink/slug (cached)ok example.com/shortlink/store (cached)這裡有兩個容易忽略的細節。
第一,快取條目是不可變的。 key 命中就不會再儲存5,日誌裡寫著 Cache hit occurred on the primary key ..., not saving cache.。我們的快取是第二輪(第一次全部透過)存下的,之後每一輪恢復的都是那一份快照,直到 go.sum 變化或者 7 天沒人用。程式碼越改越多,這份快照能幫上的忙越少。
第二,store (cached) 意味著整合測試根本沒有連資料庫。 這一輪 job 照樣花了 10 秒起 Postgres 並等它健康檢查透過,但 go test 發現測試二進位制、引數和讀過的環境變數(DATABASE_URL)都和上次一樣,就直接把上次的輸出重放了一遍10。Go 的測試快取不知道外面那個資料庫是新起的。
Dagger:每個操作都有摘要
Dagger 引擎裡,每個操作的輸入(映象摘要、掛載目錄的內容雜湊、命令列、環境變數)合起來算出一個摘要,摘要相同就直接取上次的結果,日誌裡標 CACHED。原樣再跑一遍,兩個測試操作都是:
withExec otelgotest ./slug (experimentalPrivilegedNesting: true)Container.withExec CACHED [0.0s]withExec otelgotest ./store (experimentalPrivilegedNesting: true)Container.withExec CACHED [0.0s](otelgotest 是 Go 模組用來包一層 go test、給每個測試發 OpenTelemetry span 的執行器。)
改 README 什麼都沒重跑,因為 README 不在任何一個操作的輸入裡。 Go 模組掛進測試容器的只有 Go 原始碼、go.mod/go.sum 和 testdata/2,README 變了,摘要不變。這是 GitHub Actions 那種“push 一次就整個 job 重來”做不到的。
改 slug.go 時,store 的測試操作也重跑了。 輸入粒度是“這個 Go 模組的原始碼目錄”,不是“這個包依賴的檔案”,slug.go 變了,所有包的測試容器掛載的目錄都變了。重跑之後,容器裡掛著的 Go 構建快取卷又判斷了一次,兩個包都輸出 (cached):只加註釋,編譯出來的測試二進位制沒變。也就是說,Dagger 的摘要快取擋掉了“完全沒變”的情況,剩下的細粒度判斷仍然交給 Go 自己。
新機器上,引擎的快取是空的
再看錶裡最後一列。每次 push 換一臺新 runner,引擎也是新的,/var/lib/dagger 是空的,於是每一輪都是冷啟動,要 160 秒左右;原生 GitHub Actions 同樣的場景只要 27 秒左右。
冷啟動慢在哪?日誌裡最長的是 golangci-lint 那個操作,2 分 10 秒,原生 GitHub Actions 裡同一個 lint 冷啟動只要 31 秒。再往下看,這個操作的工作目錄是 .dagger/modules/pgbase:lint 模組把我們為 Postgres 寫的那個 Dagger 模組也當成專案的一部分檢查了。dagger module init go 會在倉庫裡生成一個獨立的 Go 模組(自己的 go.mod,Go 1.26,依賴整套 OpenTelemetry),外加 dagger.gen.go 和 internal/dagger/ 下幾千行繫結程式碼,lint 的時間幾乎都花在這份生成程式碼上。
在 dagger.toml 裡把它排除掉:
[modules.golangci-lint.settings]lint = ["!.dagger"](還要給 pgbase 目錄放一個自己的 .golangci.yml,它才會成為一個能被排除的“專案”。)第七輪冷啟動降到 101 秒,lint 60 秒。剩下比原生慢的部分,主要是 Dagger 在 golang:1.24-alpine 裡開著 cgo 從頭編譯,而 runner 上的 setup-go 用的是預編譯好的工具鏈;另外 Go 模組每次還要 go install 一個 otelgotest。
所以把 Dagger 放進普通的 GitHub Actions job,在“每次一臺新機器”這個前提下,它最大的長處(操作級快取)基本用不上。 要用上,就得讓引擎活得比 job 長:自建一臺常駐的引擎機器,或者用 Dagger Cloud。這正是博文後半部分推遠端執行和 Cloud Engines 的原因。
一個意外:磁碟不夠時,引擎自己在清快取
實驗第三輪出了一個怪現象:同一臺機器上原樣再跑一遍,dagger check 照樣花了 192 秒,幾乎什麼都沒快取住。
翻引擎日誌,裡面有 1455 條 dagql pruned result。那臺 runner 的系統盤 72 GB 只剩 7.5 GB,不到 20%,而引擎預設的 minFreeSpace 就是 20%11:剩餘空間低於它,引擎就不停清理快取。GitHub 的 ubuntu-latest 映象預裝了大量工具鏈,剩餘空間每臺機器不太一樣(第四輪那臺剩 13 GB)。在 job 開頭刪掉 /usr/share/dotnet、/usr/local/lib/android 等目錄,騰到 33 GB 以後,同一個場景從 192 秒降到 9 秒。
在 GitHub 託管的 runner 上跑 Dagger,要麼先騰空間,要麼調低引擎的 gc.minFreeSpace,否則快取會在你眼皮底下消失。
快取決定了“重算多少”,但整合測試還有一個更麻煩的問題:它要的資料庫是誰起的,又是誰決定要不要真的連一次?
四、整合測試的依賴
GitHub Actions:job 級的服務容器
原生 GitHub Actions 的 Postgres 寫在 services: 裡。runner 在執行任何 step 之前先做一步 “Initialize containers”:拉映象、建網路、起容器,再按 --health-cmd pg_isready 每 2 秒問一次,直到健康。這一步在我們幾輪裡用了 10 到 17 秒。容器一直活到 job 結束才刪掉12。
連線地址取決於 job 跑在哪:job 直接跑在 runner 上,就用 localhost:5432,要靠 ports 把埠對映出來;job 跑在容器裡,就用服務名 postgres。這個區別後面在本地跑的時候還會碰到。
Dagger:繫結在測試容器上的服務
Dagger 裡,Postgres 是 pgbase 返回的那個容器的一個依賴:WithServiceBinding("db", pg) 讓測試容器裡能用主機名 db 連到它。服務不是先起好等著,而是在第一個需要它的操作開始時才啟動,健康檢查透過後才放測試進去;同一個服務定義算出同一個主機名,多個操作共用一個例項13。日誌裡能看到測試操作在等它:Container.withExec DONE [0.6s, waited on service.start]。
相同點比想象的多:兩邊最後都栽在 Go 自己的測試快取上。 Dagger 的摘要快取在“什麼都沒變”時連 Postgres 都不起,這比 GitHub Actions 起了 10 秒資料庫卻不用要省;可一旦原始碼變了,測試操作重跑,容器裡掛著的 Go 構建快取卷會照樣回答 store (cached)。對 Go 來說,DATABASE_URL 沒變、測試二進位制沒變,就是同一個測試。
還有一個 Dagger 特有的細節:base 是整個 Go 模組共用的,所以 slug 的純函式測試也綁上了 Postgres,只選 TestEncodeZero 一個測試也要等它起來(1.4 秒)。想讓不同的包用不同的容器,要麼拆模組,要麼自己寫模組函式。
讓整合測試每次真的跑
Go 的標準做法是 -count=110。原生 GitHub Actions 在 step 上設 GOFLAGS=-count=1;Dagger 在 Go 模組設定里加 goflags:
dagger module settings go goflags -- -count=1第七輪兩邊都加上以後,原生 GitHub Actions 的輸出變成了 ok example.com/shortlink/store 0.028s,測試真的連了資料庫;Dagger 改了 slug.go 以後也是 store 0.021s。
但 Dagger 原樣再跑一遍時,兩個測試操作仍然是 CACHED,-count=1 根本沒機會生效:輸入一個位元組都沒變,引擎直接拿上次的結果。這是 Dagger 有意的設計:它把一次測試看成“原始碼 + 容器 + 服務定義”的函式,Postgres 每次都是從同一個映象新起的,輸入不變,結果就不該變。對於依賴外部真實服務、本身不穩定的測試,這個假設就不成立了,這類測試要麼搬進容器裡的服務,要麼想辦法讓輸入每次都不同。
資料庫的問題解決了,下一個問題是:測試真的掛了,我們看到的是什麼,又能多快地只重跑那一個?
五、測試失敗以後
我們把 TestEncodeZero 的期望值改錯,兩邊各跑一次。
GitHub Actions 看到的單位是 step。 它知道 go test 這個 step 退出碼是 1,至於是哪個測試,要人(或者 agent)去讀日誌。重跑的最小單位是 job14:“Re-run failed jobs” 會把 checkout、起 Postgres、lint 全部再來一遍。
Dagger 看到的單位是測試。 Go 模組用 otelgotest 包住 go test,每個測試發一個 OpenTelemetry span,所以報告裡直接列出 TestEncodeZero FAIL 和失敗的那一行,還附了一條“在本地復現”的命令。選中單個測試重跑時,模組實際執行的是 otelgotest -run '^(TestEncodeZero)$' ./slug,在快取熱著的引擎上 5.9 秒。
博文說的“日誌又長又沒有結構,agent 要燒 token 從文字里重建上下文”,指的就是左邊這種情況。我們這個專案日誌很短,讀起來不費勁;但對於幾千個測試、幾十個 job 的倉庫,“哪個測試掛了”能不能直接拿到,差別就大了。
失敗後還能再進一步:dagger check -i 會在失敗的那個容器狀態裡開啟一個 shell15,相當於 GitHub Actions 裡大家常用的 tmate 遠端除錯 action。這個需要互動終端,我們在 CI 裡沒法演示。
能在 CI 上精確重跑一個測試固然好,但更省事的是根本不 push:在本地跑同一套檢查。
六、在本地跑同一套
GitHub Actions:用 act 模擬
GitHub Actions 沒有官方的本地執行方式,常用的是 act16:它讀 workflow 檔案,用 Docker 起一個模擬 runner 的容器,把 action 下載下來在裡面執行。我們在一臺 runner 上用 act v0.2.89 跑同一個 native job,映象用 catthehacker/ubuntu:act-latest:
- 總共 129 秒,其中 setup-go 12 秒、lint 28 秒、測試 3 秒,剩下八十多秒主要花在拉映象和起容器上;
- act 裡沒有 GitHub 的快取服務,setup-go 報
Cache is not found,每次都是冷的; - 第一次我們以為 act 的 job 跑在容器裡,把資料庫地址改成了服務名
postgres,結果lookup postgres on 127.0.0.53:53: server misbehaving;改回和 GitHub 上一樣的localhost:5432才透過。
act 能跑通大部分 workflow,但它是一個“儘量像”的模擬器:runner 映象、網路、快取、GITHUB_TOKEN 的許可權,每一處都可能和真的不一樣。
Dagger:本地和 CI 是同一條命令
Dagger 這邊沒有“模擬”這一層。CI 裡執行的是 dagger check,本地執行的也是 dagger check,背後是同一個引擎、同一份 dagger.toml 和 dagger.lock、同樣的容器映象。區別只在引擎放在哪:本地是筆記本上的一個容器,CI 裡是 runner 上的一個容器。博文把這一點放在第一位,理由是 coding agent:agent 在本地就能跑出和 CI 完全一樣的結果,就不用 “push and pray”,每次推上去等結果、讀日誌、再改1。
要付出的代價是:測試必須能在容器裡跑,依賴要寫成模組。 我們為 store 測試寫了 pgbase,它帶來了一個獨立的 Go 模組和幾千行生成程式碼,還差點把 lint 拖慢一倍(第三節)。這些都要提交到倉庫裡,Dagger 才能載入它們。
本地這一層,Dagger 贏在“同一套”。但開頭那句 “replaces GitHub Actions” 說的是另一件事:不要 runner、不要 workflow 檔案,由 Dagger 直接接 GitHub 的事件。這一層替掉以後,賬怎麼算?
七、Cloud Checks 和按用量計費
Cloud Checks 接走的是哪一層
執行 dagger cloud checks on 以後,Dagger Cloud 透過 GitHub App 接收 push 和 PR 事件,在 Cloud Engines 上跑 dagger check,結果寫回成 GitHub 的 check status17。對照圖 2,這一步把“觸發”“機器”“快取”三層都從 GitHub Actions 手裡接走了,GitHub 只剩下程式碼託管和展示 check 結果。快取也不再隨 runner 消失:開發機、agent、CI 連的是同一批引擎,用的是同一份快取。這正好補上了第三節那個“新機器上快取是空的”的問題。
我們沒有開 Dagger Cloud 賬號,下面的賬是按公開價格推算的,沒有實測。
兩種計價方式
| 專案 | GitHub Actions 託管 runner | Dagger Cloud |
|---|---|---|
| 按什麼算 | 整臺機器 × job 時長,按分鐘向上取整 | pipeline 實際用掉的 CPU、記憶體(來自 cgroup),以及進出的資料量 |
| 單價 | Linux 2 核 $0.006/min 4 核 $0.012/min | CPU 0.5¢/CPU-min 記憶體 0.5¢/GB-min 拉入 2.5¢/GB,推出 10¢/GB |
| 機器閒著 | 照算 | 不算 |
| 重複的工作 | 省下的是時間,快了就少算 | 30 天內輸入相同的操作按五分之一算,不管實際有沒有命中快取 |
| 起步 | 套餐內的免費分鐘用完後按量 | 按量與套餐價取高者:$20/月起,團隊 $200/月起 |
均為 2026 年 10 月的公開價格,見文末參考資料。
把兩種演算法放到同一分鐘上比一比(推算)。一臺 2 核 runner 滿負荷跑一分鐘,GitHub 收 $0.00618;同樣的活放在 Dagger Cloud19,2 個 CPU-min 是 1¢,假設佔 2 GB 記憶體又是 1¢,合計 2¢,是 GitHub 的三倍多。反過來,如果這一分鐘裡機器大半在等(等資料庫起來、等網路、等別的 job),或者大部分操作是“重複工作”,Dagger 那邊就只收很少。
所以 Dagger 的賬只在兩種情況下划算:機器利用率低,或者重複工作多。 博文舉的例子是某客戶 9 月的用量在 Dagger 上 $94,按分鐘計費的 runner 估算 $19719,這是廠商自己的數字。我們這個專案,原生 job 二三十秒,按分鐘取整每次 $0.006,一個月推幾百次也到不了 Dagger 的 $20 起步價。博文瞄準的是另一種規模:coding agent 一天推幾百個相似的改動,大部分工作和昨天一模一樣。
最後一個問題:除了這兩個,同一個領域裡還有哪些工具,它們各站在哪一層?
八、同類工具的位置
| 工具 | 管哪幾層 | 快取的單位 | 本地跑同一套 |
|---|---|---|---|
| CI 平臺:觸發 + 排程 + 機器 + 按步驟執行 | |||
| Jenkins | 全部,機器自己管 | 常駐機器的工作目錄,外掛各自快取 | 有限(jenkinsfile-runner) |
| GitHub Actions | 全部,託管或自託管 runner | 按 key 存取的 tar 包 | 靠 act 模擬 |
| GitLab CI | 全部,runner 可自託管 | 按 key 存取的目錄,可放在 runner 本地或物件儲存 | gitlab-runner exec 已廢棄 |
| CircleCI | 全部,託管機器 | 按 key 存取的目錄;Docker 層快取另付費 | 本地 CLI 只能跑單個 job |
| Buildkite | 觸發與排程託管,機器自己出 | 常駐 agent 的本地磁碟 | 有限 |
| Tekton / Argo Workflows | 排程與執行,跑在 Kubernetes 上 | 掛載的卷,自己管 | 要一個叢集 |
| 執行引擎:把構建和測試變成可快取的圖,在任何 CI 裡呼叫 | |||
| Dagger | 執行 + 快取;Cloud Checks 再加觸發和機器 | 每個容器操作,按輸入摘要 | 同一條命令 |
| Earthly | 執行 + 快取,基於 BuildKit | Earthfile 裡的每一步,像 Dockerfile 的層 | 同一條命令 |
| Bazel | 構建與測試,遠端快取和遠端執行 | 每個 action,按輸入雜湊 | 同一條命令 |
| Nix | 構建與環境 | 每個 derivation,按輸入雜湊 | 同一條命令 |
只有 GitHub Actions、act 和 Dagger 在本文實測過,其餘按官方文件整理。Earthly 已於 2025 年 7 月停止維護,社群分叉為 EarthBuild。
這張表把開頭的問題說清楚了。Dagger 首先是一個執行引擎,和 Bazel、Nix、Earthly(已停更20)站在一起:把工作拆成按輸入摘要快取的小塊,本地和 CI 用同一套。它和 Bazel 的區別在於單位是容器操作而不是語言級的編譯動作,不需要把整個專案改寫成 BUILD 檔案,代價是快取粒度粗一些(我們看到的是“一個 Go 模組的原始碼目錄”)。加上 Cloud Checks 以後,它才伸到 GitHub Actions 那一行,開始和 CI 平臺搶觸發和機器。
對於用 Go 的我們,還有一點值得一提:Go 工具鏈自帶了相當好的構建和測試快取,第三節裡真正判斷“哪個包沒變”的一直是它。Dagger 在 Go 專案上的增量,主要是“完全沒變就整個跳過”“README 這類無關改動不觸發”,以及跨機器共享快取,而不是比 Go 本身更細的增量編譯。
小結
回到開頭的問題:Dagger 和 GitHub Actions 是不是同一類東西?
- 兩份配置描述的東西不同。 GitHub Actions 寫“在哪臺機器上按什麼順序執行哪些命令”;Dagger 只寫裝了哪些模組,測哪些包、哪些測試由模組從
go.mod自己找出來。相同的是,兩邊最後都變成 Linux 上的容器和程序。 - 它們原本不在同一層。 GitHub Actions 管觸發、機器、執行、快取四層;Dagger 引擎只管執行和快取,最常見的用法是放進 GitHub Actions 的一個 step 裡。博文說的“替代”,指的是 Cloud Checks:由 Dagger Cloud 直接接 GitHub 事件、用自己的引擎跑。
- Dagger 的快取是按操作的輸入摘要算的。 引擎活著時,原樣重跑 8.9 秒、改 README 什麼都不重跑;但它的粒度是“整個 Go 模組的原始碼目錄”,更細的判斷仍然交給 Go 自己。放進每次都換新機器的 GitHub Actions job 裡,快取每次都是空的,冷啟動比原生慢(101–167 秒對 27–56 秒)。託管 runner 磁碟剩餘不到 20% 時,引擎還會自己清掉快取。
- 整合測試的資料庫,兩邊起法不同,坑卻一樣。 GitHub Actions 在 job 開頭起好、開到結束;Dagger 在測試操作需要時才起,快取命中時根本不起。兩邊都會被 Go 的測試快取騙過:
store (cached)時並沒有連資料庫,要加-count=1。 - 測試失敗時,GitHub Actions 給一段日誌和一個紅色的 step,最小重跑單位是 job;Dagger 給出哪個測試掛了,能只重跑那一個(5.9 秒)。 對要從結果裡找上下文的 coding agent,這個差別比對人更大。
- 本地跑同一套:GitHub Actions 只能用 act 模擬(129 秒,地址寫法都可能不一樣);Dagger 本地和 CI 是同一條命令。 代價是依賴要寫成模組,生成程式碼要進倉庫。
- 計費方式反過來了。 GitHub 按整臺機器的時間收錢,2 核每分鐘 $0.006;Dagger Cloud 按實際消耗的 CPU 和記憶體收錢,滿負荷時更貴,閒著和重複工作時便宜。它押注的是 agent 帶來的大量重複工作。
- 在同類工具裡,Dagger 和 Bazel、Nix、Earthly 是一類(按輸入摘要快取的執行引擎),加上 Cloud Checks 以後才開始和 GitHub Actions、GitLab CI 這些平臺正面競爭。
如果想自己試,實驗用的專案、pgbase 模組和兩邊的工作流就是上文圖 1 裡那些;在一臺有 Docker 的機器上,dagger init、dagger module install dagger.io/go、dagger check 三條命令就能看到第三節的快取行為。
參考資料
參考資料
-
Dagger 1.0 betadagger.io ↗本文分析的博文。TLDR:“Dagger is third-generation CI, after Jenkins and GitHub Actions.”https://dagger.io/blog/dagger-10-beta/ ↩ ↩2
-
Go moduledocs.dagger.io ↗按 go.mod 發現 Go 模組,包是按目錄分的集合,測試是按函式名分的集合;每個包單獨跑一次
go test;base設定可以換成繫結了服務的容器。https://docs.dagger.io/reference/modules/go ↩ ↩2 -
Checking your codedocs.dagger.io ↗
dagger check -l --all列出每個檢查;--go-package、--go-test選到單個包、單個測試。https://docs.dagger.io/using/checking ↩ -
Module wiringdocs.dagger.io ↗
dag://<module>/<function>地址把一個模組的返回值注入另一個模組的設定。https://docs.dagger.io/config/module-wiring ↩ -
actions/cachegithub.com ↗“If the provided key matches an existing cache, a new cache is not created.” 每個倉庫 10 GB,7 天未訪問的條目被淘汰。https://github.com/actions/cache ↩ ↩2
-
Project Theseus: removing BuildKitdagger.io ↗v0.21 起用自己的 DAG 引擎替換 BuildKit 的 solver,新快取基於 e-graph。https://dagger.io/changelog/theseus ↩
-
Much faster (v1.0 beta)dagger.io ↗所有官方模組和 SDK 執行時都用 Dang 寫;crun 替換 runc;1000 個容器並行從 35–61 s 降到 16–17 s。https://dagger.io/changelog/v1-performance ↩
-
The Great CI Bottleneck of 2026dagger.io ↗“CI workloads are dumb, and running dumb workloads is wasteful.” 認為瓶頸是軟體問題,不是機器不夠快。https://dagger.io/blog/the-great-ci-bottleneck-of-2026/ ↩
-
actions/setup-gogithub.com ↗預設開啟快取,快取 GOMODCACHE 和 GOCACHE,key 裡帶 go.sum 的雜湊。https://github.com/actions/setup-go ↩
-
go test: test cachingpkg.go.dev ↗結果按測試二進位制、命令列引數和測試讀過的環境變數、檔案快取;“The idiomatic way to disable test caching explicitly is to use -count=1.”https://pkg.go.dev/cmd/go#hdr-Test_packages ↩ ↩2
-
engine.jsondocs.dagger.io ↗預設
minFreeSpace為磁碟的 20%:剩餘空間低於它,引擎就清理快取直到恢復。https://docs.dagger.io/reference/config-files/engine-json ↩ -
About service containersdocs.github.com ↗job 直接跑在 runner 上時用 localhost 加對映埠訪問服務;job 跑在容器裡時用服務名。服務隨 job 建立、隨 job 銷燬。https://docs.github.com/en/actions/use-cases-and-examples/using-containerized-services/about-service-containers ↩
-
Running servicesdocs.dagger.io ↗服務容器的主機名由定義算出來;第一次被用到時才啟動,沒人引用就停;客戶端連上之前先做健康檢查。https://docs.dagger.io/using/services ↩
-
Re-running workflows and jobsdocs.github.com ↗最小的重跑單位是 job:可以重跑全部 job 或失敗的 job,不能只重跑某個測試。https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/re-running-workflows-and-jobs ↩
-
Dev shellsdagger.io ↗
dagger shell進入工作區提供的任意容器;dagger check -i在命令失敗的那個狀態裡開一個 shell。https://dagger.io/changelog/dev-shells ↩ -
nektos/actgithub.com ↗在本機用 Docker 模擬 GitHub Actions 執行 workflow。文中用 v0.2.89。https://github.com/nektos/act ↩
-
Cloud Checksdagger.io ↗每次 push 和 PR 在 Cloud Engines 上跑
dagger check,結果顯示為 GitHub check status。https://dagger.io/changelog/cloud-checks ↩ -
Actions runner pricingdocs.github.com ↗Linux 2-core x64 每分鐘 0.006 美元,4-core 0.012 美元,按整臺機器、整段 job 時長計費。https://docs.github.com/en/billing/reference/actions-runner-pricing ↩
-
Dagger Cloud pricingdagger.io ↗CPU 0.5¢/CPU-min,記憶體 0.5¢/GB-min;30 天內同輸入的“重複工作”五分之一價;Starter 每月 20 美元,Scale 每月 200 美元,按用量和套餐價取高者。https://dagger.io/pricing ↩ ↩2
-
Earthly is no longer actively maintainedgithub.com ↗2025-07-16 起停止維護,Earthly Cloud 關閉;社群分叉為 EarthBuild。https://github.com/earthly/earthly ↩