Dagger 是一个可移植的 CI/CD 引擎,它把流水线即代码(Pipeline as Code)真正落地:你用 Python、Go 或 TypeScript 写一个 Module,在里面定义若干 Function,Dagger 引擎会在容器里逐步执行并自动构建出一张有向无环图(DAG)。它最大的价值是——同一份流水线代码,本地能跑、CI 里也能跑,彻底消灭”本地能跑、CI 就挂”的环境差,让构建、测试、打镜像在每台机器上行为完全一致。
一、为什么需要 Dagger:CI 与本地的一致性困境
传统 CI 有两个老毛病。第一,语法碎片化:GitHub Actions 写 YAML、GitLab CI 写另套 YAML、Jenkins 又是 Groovy,换平台几乎重写;第二,调试靠猜:流水线只能在云端排队跑,本地复现不了,一次红就得再等十几分钟。即便用 Makefile + docker-compose 凑合,也难保证”云端容器”和”我笔记本的容器”是同一套依赖。
Dagger 的思路很干脆:把每一步都跑在标准化的 OCI 容器里,流水线本身用普通代码写,由统一的 Dagger 引擎调度。于是本地执行和云端执行走的是同一条代码路径,差异被天然抹平。如果你已经在用 GitHub Actions 搭建 CI/CD 流水线,Dagger 可以成为它里面”真正干活”的那一层。
举个真实例子:本地用 Go 1.22 编译通过,CI 镜像却是 golang:1.21,依赖了新语法直接报错;或者本地 macOS 上跑通的 Shell 脚本,在 CI 的 Ubuntu 里因为某个 GNU 工具参数不同行为迥异。这类问题在 Dagger 里几乎不会发生,因为编译步显式写了 golang:1.22,本地和 CI 拉的是同一个镜像摘要,连底层 glibc 版本都锁死。
二、Dagger 核心概念:Engine、Module、Function
动手前先理清三个名词:
- Engine(引擎):一个常驻容器,负责把你的流水线编译成 DAG、调度每一步、管理缓存。本地和 CI 都连同一个引擎。
- Module(模块):一个代码包,声明式地描述你能”调用什么”。每个项目通常一个 Module,对应一个
dagger.json。 - Function(函数):Module 里暴露的可调用单元,比如
build、test、publish,用普通方法定义,参数和返回值都是强类型。
每一步操作(拉镜像、拷贝目录、执行命令)都会生成一个新的容器层,Dagger 据此自动推断依赖关系并并行执行无依赖的步骤,这也是它比线性 Shell 脚本快的原因。
三、快速上手:安装与初始化模块
只需一个二进制,无需在仓库里塞一堆 CI 配置:
# 安装 Dagger CLI(会自动下载引擎)
curl -fsSL https://dl.dagger.io/install.sh | sh
sudo mv ./bin/dagger /usr/local/bin/
# 验证
dagger version
# 在项目根目录初始化一个 Python SDK 模块
dagger init --name my-ci --sdk python
执行后生成 dagger.json 与 ci/main.py 骨架。dagger.json 内容非常简单:
{
"name": "my-ci",
"sdk": "python",
"source": "ci"
}
四、用 Python SDK 编写第一条流水线
编辑 ci/main.py,用 @object_type 定义模块、用 @function 暴露函数。下面以最常见的 Go 服务为例,给出编译与测试两个 Function:
import dagger
from dagger import function, object_type
@object_type
class Ci:
@function
async def build(self, source: dagger.Directory) -> dagger.Container:
"""编译 Go 程序,返回包含产物的容器"""
return (
dag.container()
.from_("golang:1.22")
.with_directory("/src", source)
.with_workdir("/src")
.with_exec(["go", "mod", "download"])
.with_exec(["go", "build", "-o", "/bin/app", "."])
)
@function
async def test(self, source: dagger.Directory) -> str:
"""运行单元测试,返回标准输出"""
return await (
dag.container()
.from_("golang:1.22")
.with_directory("/src", source)
.with_workdir("/src")
.with_exec(["go", "test", "./...", "-race", "-v"])
.stdout()
)
关键点:source: dagger.Directory 是调用方传进来的源码目录;dag.container() 每次都从一个干净的基础镜像开始,保证可复现;with_exec 的返回值是新容器,链式调用天然形成 DAG。构建环境用容器隔离,和 Docker Compose 一键编排 的思路一脉相承。
五、本地与 CI 跑同一行命令
本地调试时,直接把当前目录作为 source 传入:
# 编译并导出二进制到本地 ./dist
dagger -m ./ci call build --source . --out ./dist
# 跑测试,输出直接打到终端
dagger -m ./ci call test --source .
注意:这条命令在笔记本上和在任何 CI 里完全一致。你不用再为了”CI 挂了”而登服务器、翻日志,本地就能 100% 还原整条流水线。配合 VS Code 插件 还能在编辑器里直接调 Function,调试体验远超 YAML。
六、接入 GitHub Actions:实质只换一层壳
CI 平台只负责”拉代码 + 调 Dagger”,真正的逻辑全在 Module 里。把 CI 文件缩减到极简:
name: ci
on: [push]
jobs:
dagger:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Dagger pipeline
uses: dagger/dagger-for-github@v6
with:
version: "0.13"
module: ci
args: call test --source .
cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }}
如果团队用的是 GitLab,只需把上面的 action 换成一行 dagger call 即可,Module 代码一行都不用改——这正是 Dagger 可移植性的体现,详见 GitLab CI/CD 实战。部署侧可以继续交给 ArgoCD 做 GitOps 持续部署,Dagger 专注”构建到镜像”,职责清晰。
七、发布镜像与缓存加速
再加一个 publish Function,把构建结果直接推到镜像仓库:
@function
async def publish(self, source: dagger.Directory, registry: str, tag: str) -> str:
"""构建并推送镜像,返回可读地址"""
return await (
self.build(source)
.with_entrypoint(["/bin/app"])
.publish(f"{registry}:{tag}")
)
调用:dagger -m ./ci call publish --source . --registry registry.example.com/myapp --tag v1.0.0。推送凭证通过 --docker-cfg 或 --secret 注入,避免明文写在代码里。
速度方面,Dagger 默认会缓存每一步容器层;接入 Dagger Cloud(设置环境变量 DAGGER_CLOUD_TOKEN)后还能跨机器、跨流水线共享远程缓存,多次构建只重跑真正变化的步骤,CI 时长往往能砍掉一半以上。
八、Dagger 与传统 CI 的取舍
| 维度 | 传统 CI YAML | Make + docker-compose | Dagger |
|---|---|---|---|
| 本地/云端一致性 | 弱(环境难复现) | 中(依赖本机 Docker) | 强(同引擎同容器) |
| 跨平台移植 | 弱(各家语法不同) | 强 | 强 |
| 类型安全与补全 | 无 | 无 | 有(SDK 强类型) |
| 调试体验 | 差(靠云端重试) | 中 | 好(本地直接调) |
| 缓存加速 | 需自己配 | 手动 | 内置 + 远程缓存 |
九、生产落地检查清单
| 检查项 | 建议 |
|---|---|
| Module 代码入库 | dagger.json 与 SDK 代码随业务仓库版本化 |
| 密钥管理 | 用 --secret / Dagger Cloud Secret,禁止明文 |
| 缓存 | CI 设置 DAGGER_CLOUD_TOKEN 开启远程缓存 |
| 固定引擎版本 | CI 与本地锁定同一 version,防漂移 |
| 失败可见性 | 让 Dagger 返回退出码,CI 才能正确标红 |
| 前端项目复用 | 构建链路可复用 前端构建工具演进 里的 Vite/Rspack 经验 |
十、常见误区与排障
落地时最容易踩的坑集中在四处,提前规避能省下大量排查时间:
| 现象 | 根因 | 解决 |
|---|---|---|
| 本地能跑、CI 报找不到 Docker | CI 运行器未暴露 Docker socket | 用官方 Dagger 动作或手动挂载 /var/run/docker.sock |
| 推送镜像 401 / 403 | 仓库凭证未注入 | 用 --secret 或 Dagger Cloud Secret 传 docker-cfg |
| 缓存不命中、每次全量构建 | 未设置 DAGGER_CLOUD_TOKEN | CI 与本地同时配置远程缓存令牌 |
| 某步卡住无日志 | 容器未输出到 stdout | 加 dagger call --debug 看引擎执行详情 |
调试时优先用 dagger call --debug 打开引擎日志,再用 dagger shell 进入交互式会话逐步执行 Function,能精确定位是哪一步的容器拉不起来或命令失败。网络方面,Dagger 引擎本身通过本地 Docker 运行,只要宿主机能拉镜像,流水线就能跑,无需额外放行端口;这也意味着它和 Docker Compose 编排 一样,对网络环境的要求极低。
十一、结语
Dagger 不是要取代 GitHub Actions 或 GitLab CI,而是把”真正易变、最容易出环境问题的构建逻辑”收进一份可测试、可移植的代码里。当你能在笔记本上用一条 dagger call 跑完整条流水线,再把它原样塞进任意 CI,那种”改完立刻知道对错”的顺畅感,会让你再也回不去手写 YAML 的日子。对于追求交付确定性的团队,Dagger 值得作为流水线即代码的首选底座。




