1. 项目概述为什么我们需要一个“更强大”的镜像构建工具如果你和我一样在容器化和云原生这条路上摸爬滚打了好几年那你一定对 Dockerfile 又爱又恨。爱它是因为它用一套简单的语法彻底改变了我们打包和分发应用的方式恨它是因为随着项目规模扩大、构建逻辑复杂化Dockerfile 很快就暴露了它的局限性缓存机制脆弱、构建步骤难以复用、跨平台构建繁琐、与 CI/CD 流水线集成时常常“水土不服”。每次为了优化一个多阶段构建的缓存命中率或者为了在团队中统一构建流程而编写一堆辅助脚本时我都在想有没有一个工具能把构建这件事做得更“工程化”一些直到我遇到了 Earthly。它不是一个简单的 Dockerfile 替代品而是一个全新的构建自动化框架。你可以把它理解为一个“超级构建器”它吸收了 Dockerfile 的容器化构建思想同时引入了 Makefile 的依赖管理和声明式任务定义最终目标是让构建过程像代码一样可维护、可测试、可复用。官方称它为“新一代更强大的镜像构建工具”这个“更强大”究竟体现在哪里简单来说它试图解决我们在日常开发中遇到的所有构建痛点不可靠的缓存、难以共享的构建逻辑、复杂的多架构支持以及构建与 CI 的割裂。在接下来的内容里我不会只给你罗列 Earthly 的语法那和看官方文档没区别。我会以一个资深 DevOps 工程师的视角带你深入拆解 Earthly 的设计哲学手把手演示如何将一个真实的中型项目的 Dockerfile 迁移到 Earthly并分享在实际落地过程中我踩过的坑和总结出的最佳实践。无论你是正在为构建速度发愁的开发者还是负责维护整个公司构建流水线的平台工程师相信这篇深度解析都能给你带来实实在在的启发。2. Earthly 核心设计哲学与架构拆解2.1 超越 Dockerfile声明式、可复用的构建即代码Earthly 最根本的革新在于其理念。Dockerfile 本质上是指令式的它描述的是“如何做”RUN apt-get update apt-get install -y...。而 Earthly 是声明式的它描述的是“要什么”一个包含特定依赖和文件的应用镜像以及达成这个目标所需要的任务和依赖关系。这种声明式体现在它的核心文件Earthfile中。一个Earthfile由多个target目标组成每个target类似于 Makefile 中的一个任务或 Dockerfile 中的一个构建阶段。但关键在于这些target可以相互依赖、参数化并且能被其他Earthfile引用。这就将构建逻辑从一次性的脚本提升为了可组合、可版本控制的“构建模块”。举个例子你的公司可能有十个微服务每个都需要用同样的方式安装系统依赖、配置时区、设置非 root 用户。在 Dockerfile 的世界里你需要在十个地方复制粘贴同一段RUN指令。而在 Earthly 里你可以创建一个base.Earthfile定义一个叫做setup-base的 target然后所有服务的Earthfile都可以通过FROM ./basesetup-base来继承这个基础环境。当基础配置需要更新时你只需修改一处。# base.Earthfile VERSION 0.7 setup-base: FROM alpine:3.18 RUN apk add --no-cache tzdata curl bash RUN addgroup -g 1000 -S appgroup adduser -u 1000 -S appuser -G appgroup WORKDIR /app USER appuser# service-a/Earthfile VERSION 0.7 FROM ./basesetup-base # 继承基础设置 COPY src/ . RUN go build -o /app/service-a ./main.go SAVE IMAGE --push my-registry/service-a:latest这种模块化设计是 Earthly “更强大”的第一个基石。它让构建基础设施的代码复用成为了可能极大地减少了重复劳动和配置漂移。2.2 确定性与高性能革命性的缓存机制构建缓存是影响开发者体验和 CI 速度的生命线。Docker 的层缓存机制虽然强大但极其脆弱。任何指令的顺序变化、上下文文件的微小改动都可能导致缓存失效引发令人沮丧的漫长重建。Earthly 的缓存机制则聪明得多。它采用了一种基于内容寻址的缓存。简单来说Earthly 会为每一个构建步骤包括其命令、输入文件、依赖的 target计算一个唯一的哈希值。只要这个哈希值不变该步骤的输出就直接从缓存中读取完全不受步骤顺序或无关文件变化的影响。这带来了两个巨大优势真正的增量构建如果你只修改了service-a的代码那么构建系统只会重新执行与service-a相关的步骤service-b以及它们共同依赖的基础层如setup-base将直接从缓存加载。跨项目共享缓存Earthly 支持本地缓存和远程缓存如 S3、Google Cloud Storage。这意味着当团队中第一个人构建了某个依赖项后其他成员以及 CI 服务器都可以直接复用缓存结果实现“一次构建处处可用”。在实际操作中你几乎能立刻感受到这种差异。一个原本需要 10 分钟的完整构建在代码微调后的增量构建可能只需要 20 秒。对于 CI/CD 流水线这直接意味着更快的反馈循环和更低的云计算成本。注意Earthly 的缓存虽然强大但并非魔法。它依赖于对输入的精确定义。如果你在COPY指令中使用了通配符如COPY . .那么任何文件的变化都会导致该步骤缓存失效。最佳实践是尽可能精确地声明需要复制的文件例如COPY go.mod go.sum ./和COPY cmd/ cmd/。2.3 构建、测试、部署一体化Earthly 作为 CI 的“执行引擎”这是 Earthly 最具野心的部分。传统的 CI/CD 流水线如 Jenkins、GitLab CI、GitHub Actions通常将“构建”作为一个独立的 Job 或 Step里面塞满了 Docker build 命令和各种 shell 脚本。构建逻辑散落在 CI 配置文件和 Dockerfile 中难以本地复现形成了所谓的“CI 脚本魔法”。Earthly 提出了一个不同的范式用 Earthly 定义所有构建、测试、甚至部署任务而 CI 系统只负责触发 Earthly 命令。你的Earthfile里不仅可以构建镜像还可以定义单元测试、集成测试、代码质量检查、生成文档等任务。VERSION 0.7 # 构建目标 build: FROM deps COPY src/ . RUN go build -o /app/myapp ./cmd/server SAVE ARTIFACT /app/myapp AS LOCAL ./dist/myapp # 单元测试目标 unit-test: FROM deps COPY src/ . RUN go test ./... -v # 集成测试目标可能需要数据库 integration-test: FROM deps COPY src/ . WITH DOCKER --compose docker-compose.test.yml RUN ./scripts/wait-for-db.sh go test ./integration -v END # 主入口按顺序执行 all: BUILD unit-test BUILD integration-test BUILD build这样一来Earthfile成为了项目唯一的构建“真相源”。开发者可以在本地运行earthly unit-test来运行测试这与 CI 上运行earthly --ci all在本质上完全一致彻底解决了“在我机器上是好的”这个经典问题。CI 配置则变得极其简洁只剩下调用 Earthly 和上传制品等步骤。3. 从零开始将一个真实项目迁移到 Earthly理论说再多不如动手干。让我们以一个典型的 Go 语言 Web 服务项目为例将其从传统的 Dockerfile 迁移到 Earthly。假设项目结构如下my-go-service/ ├── Dockerfile ├── go.mod ├── go.sum ├── cmd/ │ └── server/ │ └── main.go ├── internal/ ├── pkg/ └── docker-compose.test.yml3.1 原始 Dockerfile 分析典型的 Dockerfile 可能是这样的# 多阶段构建 FROM golang:1.21-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED0 GOOSlinux go build -o server ./cmd/server FROM alpine:latest RUN apk --no-cache add ca-certificates tzdata WORKDIR /root/ COPY --frombuilder /app/server . EXPOSE 8080 CMD [./server]这个 Dockerfile 已经不错了利用了多阶段构建来减小最终镜像体积。但它的问题也很典型缓存完全依赖于go.mod和go.sum文件如果任何源代码文件变化go mod download之后的缓存全部失效需要重新下载所有依赖虽然 Go Module 有本地缓存但在 CI 纯净环境中仍耗时。3.2 分步迁移至 Earthfile第一步安装 Earthly根据你的操作系统安装非常简单。以 macOS 为例brew install earthly/earthly/earthly earthly bootstrap安装完成后在项目根目录初始化earthly init这会创建一个空的Earthfile。第二步创建基础依赖 Target我们将依赖安装单独抽离确保只要go.mod和go.sum不变依赖步骤的缓存就永远有效。# Earthfile VERSION 0.7 # 基础依赖阶段高度可缓存 deps: FROM golang:1.21-alpine WORKDIR /app # 精确复制依赖定义文件 COPY go.mod go.sum ./ RUN go mod download # 保存这个状态供后续 target 使用 SAVE IMAGE第三步定义构建 Target构建 target 依赖于deps并且只复制必要的源代码目录避免因文档、配置文件等无关文件变动导致缓存失效。build: # 从 deps target 的结果开始而不是从头开始 FROM deps # 复制源代码注意这里没有复制整个根目录 COPY cmd/ cmd/ COPY internal/ internal/ COPY pkg/ pkg/ # 构建 RUN CGO_ENABLED0 GOOSlinux go build -o server ./cmd/server # 将构建产物保存为本地文件 SAVE ARTIFACT server AS LOCAL ./dist/server第四步定义最终镜像 Target这是生成最终 Docker 镜像的地方它不直接依赖deps而是使用buildtarget 产出的二进制文件。docker: # 使用轻量级基础镜像 FROM alpine:latest RUN apk --no-cache add ca-certificates tzdata WORKDIR /root/ # 从 build target 复制构建好的二进制文件 COPY build/server ./ EXPOSE 8080 ENTRYPOINT [./server] # 将最终镜像推送到仓库可选通常在 CI 中执行 SAVE IMAGE --push my-registry/my-go-service:latest第五步添加测试 Target现在我们可以轻松地添加一个在一致环境中运行的测试任务。test: FROM deps COPY . . # 测试需要所有源代码 RUN go test ./... -v -count1第六步创建默认或聚合 Target通常我们会定义一个all或defaulttarget 来串联常用任务。# 默认任务运行测试和构建 default: BUILD test BUILD build现在一个完整的、模块化的Earthfile就完成了。在本地你可以运行earthly build仅构建二进制文件。earthly test运行所有测试。earthly docker构建并输出 Docker 镜像。earthly --push docker构建并推送镜像需要提前配置认证。直接运行earthly执行defaulttarget即运行测试后再构建。3.3 迁移过程中的关键决策与技巧Target 粒度划分不要把所有东西塞进一个 target。像deps、build、test、docker这样按职责分离能最大化缓存收益。一个经验法则是将变化频率不同的步骤分离到不同的 target 中。依赖文件go.mod变化少单独成 target源代码变化频繁放在后续 target。COPY 指令的艺术这是优化缓存的关键。永远优先使用精确的目录或文件列表而不是COPY . .。例如COPY cmd/ cmd/比COPY . .要好得多因为改一个 README.md 不会导致构建缓存失效。理解SAVE指令SAVE IMAGE用于输出 Docker 镜像SAVE ARTIFACT用于将容器内的文件保存到本地宿主机。这是 Earthly 与 Dockerfile 的重要区别它让你可以在构建流水线的中间阶段提取产物。本地开发与 CI 的统一在Earthfile中你可以使用ARG指令来参数化构建。例如为测试设置不同的数据库连接字符串或者为不同环境设置不同的镜像标签。这确保了本地和 CI 的行为完全一致。VERSION 0.7 docker: ARG taglatest FROM alpine:latest COPY build/server ./ SAVE IMAGE --push my-registry/my-go-service:$tag在 CI 中你可以这样调用earthly --push --tagprod-v1.0 docker。4. 高级特性与复杂场景实战4.1 跨平台构建与多架构镜像在 Dockerfile 中构建多架构镜像如 amd64, arm64通常需要配置复杂的buildx或手动编写多个 Dockerfile。Earthly 将此过程大大简化。Earthly 原生支持通过--platform标志进行跨平台构建。更重要的是它可以与docker manifest命令结合轻松创建多架构镜像清单。VERSION 0.7 docker-multiarch: # 这个 target 本身不构建它编排多个平台的构建 BUILD --platformlinux/amd64 docker --tagmyapp-amd64 BUILD --platformlinux/arm64/v8 docker --tagmyapp-arm64 # 本地构建时可以保存不同架构的镜像 SAVE IMAGE --push my-registry/myapp:latest-amd64 AS my-registry/myapp:latest-amd64 SAVE IMAGE --push my-registry/myapp:latest-arm64 AS my-registry/myapp:latest-arm64 # 在 CI 中可以继续执行创建 manifest 的命令通常通过 earthly 的 RUN 指令调用 docker cli在实际的 CI 流水线中你可能会专门有一个 Earthly target 或一个后续的 shell 脚本来使用docker manifest create将myapp:latest-amd64和myapp:latest-arm64合并为myapp:latest。实操心得对于复杂的多架构推送和 manifest 创建我更喜欢在 Earthly 中完成所有架构的构建和打标签然后在一个专门的“发布” Earthfile 或 CI 步骤中使用docker manifest工具完成最终清单的创建和推送。这样关注点分离更清晰。4.2 集成外部依赖WITH DOCKER 的威力很多项目的测试或构建需要依赖其他服务比如数据库、消息队列。Earthly 提供了WITH DOCKER指令允许你在一个构建步骤中启动一个临时的 Docker Compose 环境。VERSION 0.7 integration-test: FROM deps COPY . . # 启动测试依赖 WITH DOCKER --compose docker-compose.test.yml # 等待数据库就绪 RUN ./scripts/wait-for-it.sh db:5432 --timeout30 # 运行集成测试 RUN go test ./integration -v -count1 ENDWITH DOCKER块内的RUN指令会在一个包含了你所启动的 Docker Compose 服务网络的环境中执行。这为集成测试提供了完美的、可重复的隔离环境。测试结束后所有临时容器会被自动清理。4.3 构建流水线编排与条件执行Earthly 的 target 之间可以形成复杂的依赖图。你可以利用BUILD指令和IF、FOR等控制语句来编排复杂的构建流水线。VERSION 0.7 # 构建所有微服务 build-all: FOR service IN ./services/* BUILD $servicedocker END # 一个根据 git 分支决定行为的部署目标 deploy: IF [ $EARTHLY_GIT_BRANCH main ] BUILD deploy-prod ELSE BUILD deploy-staging END deploy-prod: # ... 生产环境部署逻辑 RUN ./deploy.sh --envprod deploy-staging: # ... 预发环境部署逻辑 RUN ./deploy.sh --envstagingEarthly 提供了一些内置的 ARG如EARTHLY_GIT_BRANCH、EARTHLY_GIT_TAG方便你在构建逻辑中根据代码仓库的状态做决策。5. 落地实践集成到 CI/CD 与团队协作5.1 在 GitHub Actions 中集成 Earthly将 Earthly 集成到现代 CI 系统中非常直观。以下是一个 GitHub Actions 工作流的示例它会在每次推送到 main 分支时运行测试、构建并推送多架构镜像。# .github/workflows/ci.yml name: CI with Earthly on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build-and-test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkoutv3 - name: Set up Earthly uses: earthly/actions/setup-earthlyv1 with: version: v0.7.22 # 建议固定版本 enable-earthly-ci: true # 启用 CI 模式 - name: Login to Container Registry run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - name: Run tests and build run: earthly --ci all # 运行 Earthfile 中的 default target - name: Build and push multi-arch image (on main) if: github.ref refs/heads/main run: | earthly --ci --push docker-multiarch # 假设 docker-multiarch target 只构建这里再创建 manifest docker manifest create my-registry/myapp:latest \ my-registry/myapp:latest-amd64 \ my-registry/myapp:latest-arm64 docker manifest push my-registry/myapp:latest关键点--ci标志Earthly 的 CI 模式会进行一些优化例如更激进的缓存清理策略并确保构建日志格式适合 CI 环境。远程缓存为了获得极致的 CI 速度强烈建议配置远程缓存。你可以在 Earthly 的~/.earthly/config.yml或通过环境变量配置一个 S3 兼容的存储后端。这样第一个合并请求触发的构建产生的缓存可以被后续的构建复用。5.2 团队协作与 Earthly Satellites对于大型团队本地构建可能因网络或机器性能差异导致体验不一致。Earthly 提供了一个企业级功能叫Satellites。你可以将其理解为一个由 Earthly 管理的、云上的共享构建“执行器”。团队开发者不再需要在本地运行沉重的构建而是通过earthly --satellite name build命令将构建任务提交到云端的 Satellite 执行。Satellite 拥有强大的计算资源、稳定的网络并且共享同一个缓存。这带来了几个好处一致的构建环境所有人都使用完全相同的环境构建彻底消灭“在我机器上可以”的问题。极快的构建速度云实例性能强大且缓存命中率极高。降低本地负载开发者的笔记本电脑不再需要运行 Docker 消耗资源。虽然 Satellites 是 Earthly 的商业功能但对于中大型工程团队它在提升开发效率和减少环境问题上的投资回报率是非常高的。5.3 迁移策略与团队培训将团队从一个成熟的 Dockerfile 工作流迁移到 Earthly需要谨慎的计划试点项目选择一个中等复杂度、构建速度慢或构建逻辑复杂的服务作为试点。用本文的步骤进行迁移并记录耗时和遇到的问题。并行运行在试点项目的 CI 中同时运行旧的 Docker 构建和新的 Earthly 构建对比产物镜像的层、二进制文件的 MD5是否一致确保正确性。知识分享为团队举办一次内部 workshop讲解 Earthly 的核心概念Target, 缓存机制SAVE指令和基本语法。重点演示它如何解决当前工作流中的痛点。编写团队规范制定团队的Earthfile编写规范。例如如何命名 target、如何划分粒度、如何使用 ARG、如何编写可复用的基础 Earthfile 等。渐进式迁移不要试图一次性迁移所有项目。按优先级逐个迁移并鼓励开发者在迁移过程中重构和优化原有的构建逻辑。6. 常见问题、性能调优与避坑指南即使有了强大的工具错误的用法也会导致问题。以下是我在多个项目中实践 Earthly 后总结的“血泪教训”。6.1 缓存不生效检查你的输入这是新手最常见的问题。“我明明只改了一行注释为什么整个depstarget 都重跑了”原因很可能是因为你的COPY指令包含了不该包含的文件。例如如果你在depstarget 里写了COPY . .那么任何文件的改动包括README.md都会改变该步骤的哈希值。解决严格限定COPY的范围。对于依赖安装只复制go.mod和go.sum。对于构建只复制src/、cmd/等源代码目录。6.2 构建速度没有想象中快未使用远程缓存本地缓存只对你自己有用。团队协作和 CI 环境中必须配置远程缓存如 S3才能实现缓存共享。这是提升 CI 构建速度最有效的一步。Target 粒度过粗如果你把下载依赖、编译代码、运行测试都放在一个 target 里那么任何代码改动都会导致“下载依赖”这个本应高度稳定的步骤缓存失效。务必拆分开。网络问题Earthly 构建时每个FROM和RUN如果涉及下载都会在容器内进行。确保你的基础镜像源和下载地址如 npm registry, pip index是高速可用的。可以在基础 Earthfile 中预先配置镜像源。6.3 如何调试 Earthly 构建earthly --verbose target输出极其详细的日志包括每个步骤的计算哈希、缓存查询结果等。这是排查缓存问题的利器。earthly --no-cache target强制忽略所有缓存全新构建。用于验证构建的确定性和正确性。earthly --save-inline-cache --push target在推送镜像的同时将缓存也推送到远程。这对于在 CI 中为后续构建准备缓存非常有用。交互式调试Earthly 目前不直接支持docker run -it那样的交互式进入容器。如果某个RUN步骤失败你需要仔细查看错误日志或者尝试将该步骤的命令拆分并在本地模拟容器环境进行测试。6.4 与现有 Docker 工具链的兼容性docker build参数Earthly 不完全支持所有docker build的参数。例如--build-arg在 Earthly 中是通过ARG指令在Earthfile内部声明的或者在命令行通过earthly --build-arg keyvalue传递。Docker ComposeEarthly 的WITH DOCKER可以启动 Compose 服务但如果你现有的开发流程严重依赖docker-compose up进行本地开发Earthly 并不会取代它。Earthly 主要负责“构建”和“测试”阶段的自动化本地开发时的服务编排可以继续使用 Docker Compose。两者是互补关系。6.5 成本考量Earthly Satellites作为商业功能需要订阅付费。你需要评估它带来的开发效率提升和本地资源节省是否值得这笔开销。对于小型团队或开源项目本地构建远程缓存可能已足够。远程缓存存储使用 S3 等云存储作为远程缓存后端会产生存储和流量费用。但通常这部分成本极低因为缓存是增量更新的且构建日志等中间产物不会存入缓存。经过几个项目的深度使用Earthly 已经彻底改变了我对构建系统的看法。它带来的最大价值不是某个单点速度的提升而是一种工程秩序的建立。构建逻辑变得清晰、可复用、可测试缓存行为变得可预测本地与 CI 环境达到高度一致。这些特性共同作用显著降低了与构建相关的维护成本和认知负担。如果你正在为混乱的 Dockerfile、缓慢的 CI 构建或是脆弱的构建流程而头疼那么投入时间学习和引入 Earthly很可能是一笔非常划算的技术投资。
Earthly:超越Dockerfile的下一代容器镜像构建工具实战指南
发布时间:2026/5/16 0:39:20
1. 项目概述为什么我们需要一个“更强大”的镜像构建工具如果你和我一样在容器化和云原生这条路上摸爬滚打了好几年那你一定对 Dockerfile 又爱又恨。爱它是因为它用一套简单的语法彻底改变了我们打包和分发应用的方式恨它是因为随着项目规模扩大、构建逻辑复杂化Dockerfile 很快就暴露了它的局限性缓存机制脆弱、构建步骤难以复用、跨平台构建繁琐、与 CI/CD 流水线集成时常常“水土不服”。每次为了优化一个多阶段构建的缓存命中率或者为了在团队中统一构建流程而编写一堆辅助脚本时我都在想有没有一个工具能把构建这件事做得更“工程化”一些直到我遇到了 Earthly。它不是一个简单的 Dockerfile 替代品而是一个全新的构建自动化框架。你可以把它理解为一个“超级构建器”它吸收了 Dockerfile 的容器化构建思想同时引入了 Makefile 的依赖管理和声明式任务定义最终目标是让构建过程像代码一样可维护、可测试、可复用。官方称它为“新一代更强大的镜像构建工具”这个“更强大”究竟体现在哪里简单来说它试图解决我们在日常开发中遇到的所有构建痛点不可靠的缓存、难以共享的构建逻辑、复杂的多架构支持以及构建与 CI 的割裂。在接下来的内容里我不会只给你罗列 Earthly 的语法那和看官方文档没区别。我会以一个资深 DevOps 工程师的视角带你深入拆解 Earthly 的设计哲学手把手演示如何将一个真实的中型项目的 Dockerfile 迁移到 Earthly并分享在实际落地过程中我踩过的坑和总结出的最佳实践。无论你是正在为构建速度发愁的开发者还是负责维护整个公司构建流水线的平台工程师相信这篇深度解析都能给你带来实实在在的启发。2. Earthly 核心设计哲学与架构拆解2.1 超越 Dockerfile声明式、可复用的构建即代码Earthly 最根本的革新在于其理念。Dockerfile 本质上是指令式的它描述的是“如何做”RUN apt-get update apt-get install -y...。而 Earthly 是声明式的它描述的是“要什么”一个包含特定依赖和文件的应用镜像以及达成这个目标所需要的任务和依赖关系。这种声明式体现在它的核心文件Earthfile中。一个Earthfile由多个target目标组成每个target类似于 Makefile 中的一个任务或 Dockerfile 中的一个构建阶段。但关键在于这些target可以相互依赖、参数化并且能被其他Earthfile引用。这就将构建逻辑从一次性的脚本提升为了可组合、可版本控制的“构建模块”。举个例子你的公司可能有十个微服务每个都需要用同样的方式安装系统依赖、配置时区、设置非 root 用户。在 Dockerfile 的世界里你需要在十个地方复制粘贴同一段RUN指令。而在 Earthly 里你可以创建一个base.Earthfile定义一个叫做setup-base的 target然后所有服务的Earthfile都可以通过FROM ./basesetup-base来继承这个基础环境。当基础配置需要更新时你只需修改一处。# base.Earthfile VERSION 0.7 setup-base: FROM alpine:3.18 RUN apk add --no-cache tzdata curl bash RUN addgroup -g 1000 -S appgroup adduser -u 1000 -S appuser -G appgroup WORKDIR /app USER appuser# service-a/Earthfile VERSION 0.7 FROM ./basesetup-base # 继承基础设置 COPY src/ . RUN go build -o /app/service-a ./main.go SAVE IMAGE --push my-registry/service-a:latest这种模块化设计是 Earthly “更强大”的第一个基石。它让构建基础设施的代码复用成为了可能极大地减少了重复劳动和配置漂移。2.2 确定性与高性能革命性的缓存机制构建缓存是影响开发者体验和 CI 速度的生命线。Docker 的层缓存机制虽然强大但极其脆弱。任何指令的顺序变化、上下文文件的微小改动都可能导致缓存失效引发令人沮丧的漫长重建。Earthly 的缓存机制则聪明得多。它采用了一种基于内容寻址的缓存。简单来说Earthly 会为每一个构建步骤包括其命令、输入文件、依赖的 target计算一个唯一的哈希值。只要这个哈希值不变该步骤的输出就直接从缓存中读取完全不受步骤顺序或无关文件变化的影响。这带来了两个巨大优势真正的增量构建如果你只修改了service-a的代码那么构建系统只会重新执行与service-a相关的步骤service-b以及它们共同依赖的基础层如setup-base将直接从缓存加载。跨项目共享缓存Earthly 支持本地缓存和远程缓存如 S3、Google Cloud Storage。这意味着当团队中第一个人构建了某个依赖项后其他成员以及 CI 服务器都可以直接复用缓存结果实现“一次构建处处可用”。在实际操作中你几乎能立刻感受到这种差异。一个原本需要 10 分钟的完整构建在代码微调后的增量构建可能只需要 20 秒。对于 CI/CD 流水线这直接意味着更快的反馈循环和更低的云计算成本。注意Earthly 的缓存虽然强大但并非魔法。它依赖于对输入的精确定义。如果你在COPY指令中使用了通配符如COPY . .那么任何文件的变化都会导致该步骤缓存失效。最佳实践是尽可能精确地声明需要复制的文件例如COPY go.mod go.sum ./和COPY cmd/ cmd/。2.3 构建、测试、部署一体化Earthly 作为 CI 的“执行引擎”这是 Earthly 最具野心的部分。传统的 CI/CD 流水线如 Jenkins、GitLab CI、GitHub Actions通常将“构建”作为一个独立的 Job 或 Step里面塞满了 Docker build 命令和各种 shell 脚本。构建逻辑散落在 CI 配置文件和 Dockerfile 中难以本地复现形成了所谓的“CI 脚本魔法”。Earthly 提出了一个不同的范式用 Earthly 定义所有构建、测试、甚至部署任务而 CI 系统只负责触发 Earthly 命令。你的Earthfile里不仅可以构建镜像还可以定义单元测试、集成测试、代码质量检查、生成文档等任务。VERSION 0.7 # 构建目标 build: FROM deps COPY src/ . RUN go build -o /app/myapp ./cmd/server SAVE ARTIFACT /app/myapp AS LOCAL ./dist/myapp # 单元测试目标 unit-test: FROM deps COPY src/ . RUN go test ./... -v # 集成测试目标可能需要数据库 integration-test: FROM deps COPY src/ . WITH DOCKER --compose docker-compose.test.yml RUN ./scripts/wait-for-db.sh go test ./integration -v END # 主入口按顺序执行 all: BUILD unit-test BUILD integration-test BUILD build这样一来Earthfile成为了项目唯一的构建“真相源”。开发者可以在本地运行earthly unit-test来运行测试这与 CI 上运行earthly --ci all在本质上完全一致彻底解决了“在我机器上是好的”这个经典问题。CI 配置则变得极其简洁只剩下调用 Earthly 和上传制品等步骤。3. 从零开始将一个真实项目迁移到 Earthly理论说再多不如动手干。让我们以一个典型的 Go 语言 Web 服务项目为例将其从传统的 Dockerfile 迁移到 Earthly。假设项目结构如下my-go-service/ ├── Dockerfile ├── go.mod ├── go.sum ├── cmd/ │ └── server/ │ └── main.go ├── internal/ ├── pkg/ └── docker-compose.test.yml3.1 原始 Dockerfile 分析典型的 Dockerfile 可能是这样的# 多阶段构建 FROM golang:1.21-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED0 GOOSlinux go build -o server ./cmd/server FROM alpine:latest RUN apk --no-cache add ca-certificates tzdata WORKDIR /root/ COPY --frombuilder /app/server . EXPOSE 8080 CMD [./server]这个 Dockerfile 已经不错了利用了多阶段构建来减小最终镜像体积。但它的问题也很典型缓存完全依赖于go.mod和go.sum文件如果任何源代码文件变化go mod download之后的缓存全部失效需要重新下载所有依赖虽然 Go Module 有本地缓存但在 CI 纯净环境中仍耗时。3.2 分步迁移至 Earthfile第一步安装 Earthly根据你的操作系统安装非常简单。以 macOS 为例brew install earthly/earthly/earthly earthly bootstrap安装完成后在项目根目录初始化earthly init这会创建一个空的Earthfile。第二步创建基础依赖 Target我们将依赖安装单独抽离确保只要go.mod和go.sum不变依赖步骤的缓存就永远有效。# Earthfile VERSION 0.7 # 基础依赖阶段高度可缓存 deps: FROM golang:1.21-alpine WORKDIR /app # 精确复制依赖定义文件 COPY go.mod go.sum ./ RUN go mod download # 保存这个状态供后续 target 使用 SAVE IMAGE第三步定义构建 Target构建 target 依赖于deps并且只复制必要的源代码目录避免因文档、配置文件等无关文件变动导致缓存失效。build: # 从 deps target 的结果开始而不是从头开始 FROM deps # 复制源代码注意这里没有复制整个根目录 COPY cmd/ cmd/ COPY internal/ internal/ COPY pkg/ pkg/ # 构建 RUN CGO_ENABLED0 GOOSlinux go build -o server ./cmd/server # 将构建产物保存为本地文件 SAVE ARTIFACT server AS LOCAL ./dist/server第四步定义最终镜像 Target这是生成最终 Docker 镜像的地方它不直接依赖deps而是使用buildtarget 产出的二进制文件。docker: # 使用轻量级基础镜像 FROM alpine:latest RUN apk --no-cache add ca-certificates tzdata WORKDIR /root/ # 从 build target 复制构建好的二进制文件 COPY build/server ./ EXPOSE 8080 ENTRYPOINT [./server] # 将最终镜像推送到仓库可选通常在 CI 中执行 SAVE IMAGE --push my-registry/my-go-service:latest第五步添加测试 Target现在我们可以轻松地添加一个在一致环境中运行的测试任务。test: FROM deps COPY . . # 测试需要所有源代码 RUN go test ./... -v -count1第六步创建默认或聚合 Target通常我们会定义一个all或defaulttarget 来串联常用任务。# 默认任务运行测试和构建 default: BUILD test BUILD build现在一个完整的、模块化的Earthfile就完成了。在本地你可以运行earthly build仅构建二进制文件。earthly test运行所有测试。earthly docker构建并输出 Docker 镜像。earthly --push docker构建并推送镜像需要提前配置认证。直接运行earthly执行defaulttarget即运行测试后再构建。3.3 迁移过程中的关键决策与技巧Target 粒度划分不要把所有东西塞进一个 target。像deps、build、test、docker这样按职责分离能最大化缓存收益。一个经验法则是将变化频率不同的步骤分离到不同的 target 中。依赖文件go.mod变化少单独成 target源代码变化频繁放在后续 target。COPY 指令的艺术这是优化缓存的关键。永远优先使用精确的目录或文件列表而不是COPY . .。例如COPY cmd/ cmd/比COPY . .要好得多因为改一个 README.md 不会导致构建缓存失效。理解SAVE指令SAVE IMAGE用于输出 Docker 镜像SAVE ARTIFACT用于将容器内的文件保存到本地宿主机。这是 Earthly 与 Dockerfile 的重要区别它让你可以在构建流水线的中间阶段提取产物。本地开发与 CI 的统一在Earthfile中你可以使用ARG指令来参数化构建。例如为测试设置不同的数据库连接字符串或者为不同环境设置不同的镜像标签。这确保了本地和 CI 的行为完全一致。VERSION 0.7 docker: ARG taglatest FROM alpine:latest COPY build/server ./ SAVE IMAGE --push my-registry/my-go-service:$tag在 CI 中你可以这样调用earthly --push --tagprod-v1.0 docker。4. 高级特性与复杂场景实战4.1 跨平台构建与多架构镜像在 Dockerfile 中构建多架构镜像如 amd64, arm64通常需要配置复杂的buildx或手动编写多个 Dockerfile。Earthly 将此过程大大简化。Earthly 原生支持通过--platform标志进行跨平台构建。更重要的是它可以与docker manifest命令结合轻松创建多架构镜像清单。VERSION 0.7 docker-multiarch: # 这个 target 本身不构建它编排多个平台的构建 BUILD --platformlinux/amd64 docker --tagmyapp-amd64 BUILD --platformlinux/arm64/v8 docker --tagmyapp-arm64 # 本地构建时可以保存不同架构的镜像 SAVE IMAGE --push my-registry/myapp:latest-amd64 AS my-registry/myapp:latest-amd64 SAVE IMAGE --push my-registry/myapp:latest-arm64 AS my-registry/myapp:latest-arm64 # 在 CI 中可以继续执行创建 manifest 的命令通常通过 earthly 的 RUN 指令调用 docker cli在实际的 CI 流水线中你可能会专门有一个 Earthly target 或一个后续的 shell 脚本来使用docker manifest create将myapp:latest-amd64和myapp:latest-arm64合并为myapp:latest。实操心得对于复杂的多架构推送和 manifest 创建我更喜欢在 Earthly 中完成所有架构的构建和打标签然后在一个专门的“发布” Earthfile 或 CI 步骤中使用docker manifest工具完成最终清单的创建和推送。这样关注点分离更清晰。4.2 集成外部依赖WITH DOCKER 的威力很多项目的测试或构建需要依赖其他服务比如数据库、消息队列。Earthly 提供了WITH DOCKER指令允许你在一个构建步骤中启动一个临时的 Docker Compose 环境。VERSION 0.7 integration-test: FROM deps COPY . . # 启动测试依赖 WITH DOCKER --compose docker-compose.test.yml # 等待数据库就绪 RUN ./scripts/wait-for-it.sh db:5432 --timeout30 # 运行集成测试 RUN go test ./integration -v -count1 ENDWITH DOCKER块内的RUN指令会在一个包含了你所启动的 Docker Compose 服务网络的环境中执行。这为集成测试提供了完美的、可重复的隔离环境。测试结束后所有临时容器会被自动清理。4.3 构建流水线编排与条件执行Earthly 的 target 之间可以形成复杂的依赖图。你可以利用BUILD指令和IF、FOR等控制语句来编排复杂的构建流水线。VERSION 0.7 # 构建所有微服务 build-all: FOR service IN ./services/* BUILD $servicedocker END # 一个根据 git 分支决定行为的部署目标 deploy: IF [ $EARTHLY_GIT_BRANCH main ] BUILD deploy-prod ELSE BUILD deploy-staging END deploy-prod: # ... 生产环境部署逻辑 RUN ./deploy.sh --envprod deploy-staging: # ... 预发环境部署逻辑 RUN ./deploy.sh --envstagingEarthly 提供了一些内置的 ARG如EARTHLY_GIT_BRANCH、EARTHLY_GIT_TAG方便你在构建逻辑中根据代码仓库的状态做决策。5. 落地实践集成到 CI/CD 与团队协作5.1 在 GitHub Actions 中集成 Earthly将 Earthly 集成到现代 CI 系统中非常直观。以下是一个 GitHub Actions 工作流的示例它会在每次推送到 main 分支时运行测试、构建并推送多架构镜像。# .github/workflows/ci.yml name: CI with Earthly on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build-and-test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkoutv3 - name: Set up Earthly uses: earthly/actions/setup-earthlyv1 with: version: v0.7.22 # 建议固定版本 enable-earthly-ci: true # 启用 CI 模式 - name: Login to Container Registry run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - name: Run tests and build run: earthly --ci all # 运行 Earthfile 中的 default target - name: Build and push multi-arch image (on main) if: github.ref refs/heads/main run: | earthly --ci --push docker-multiarch # 假设 docker-multiarch target 只构建这里再创建 manifest docker manifest create my-registry/myapp:latest \ my-registry/myapp:latest-amd64 \ my-registry/myapp:latest-arm64 docker manifest push my-registry/myapp:latest关键点--ci标志Earthly 的 CI 模式会进行一些优化例如更激进的缓存清理策略并确保构建日志格式适合 CI 环境。远程缓存为了获得极致的 CI 速度强烈建议配置远程缓存。你可以在 Earthly 的~/.earthly/config.yml或通过环境变量配置一个 S3 兼容的存储后端。这样第一个合并请求触发的构建产生的缓存可以被后续的构建复用。5.2 团队协作与 Earthly Satellites对于大型团队本地构建可能因网络或机器性能差异导致体验不一致。Earthly 提供了一个企业级功能叫Satellites。你可以将其理解为一个由 Earthly 管理的、云上的共享构建“执行器”。团队开发者不再需要在本地运行沉重的构建而是通过earthly --satellite name build命令将构建任务提交到云端的 Satellite 执行。Satellite 拥有强大的计算资源、稳定的网络并且共享同一个缓存。这带来了几个好处一致的构建环境所有人都使用完全相同的环境构建彻底消灭“在我机器上可以”的问题。极快的构建速度云实例性能强大且缓存命中率极高。降低本地负载开发者的笔记本电脑不再需要运行 Docker 消耗资源。虽然 Satellites 是 Earthly 的商业功能但对于中大型工程团队它在提升开发效率和减少环境问题上的投资回报率是非常高的。5.3 迁移策略与团队培训将团队从一个成熟的 Dockerfile 工作流迁移到 Earthly需要谨慎的计划试点项目选择一个中等复杂度、构建速度慢或构建逻辑复杂的服务作为试点。用本文的步骤进行迁移并记录耗时和遇到的问题。并行运行在试点项目的 CI 中同时运行旧的 Docker 构建和新的 Earthly 构建对比产物镜像的层、二进制文件的 MD5是否一致确保正确性。知识分享为团队举办一次内部 workshop讲解 Earthly 的核心概念Target, 缓存机制SAVE指令和基本语法。重点演示它如何解决当前工作流中的痛点。编写团队规范制定团队的Earthfile编写规范。例如如何命名 target、如何划分粒度、如何使用 ARG、如何编写可复用的基础 Earthfile 等。渐进式迁移不要试图一次性迁移所有项目。按优先级逐个迁移并鼓励开发者在迁移过程中重构和优化原有的构建逻辑。6. 常见问题、性能调优与避坑指南即使有了强大的工具错误的用法也会导致问题。以下是我在多个项目中实践 Earthly 后总结的“血泪教训”。6.1 缓存不生效检查你的输入这是新手最常见的问题。“我明明只改了一行注释为什么整个depstarget 都重跑了”原因很可能是因为你的COPY指令包含了不该包含的文件。例如如果你在depstarget 里写了COPY . .那么任何文件的改动包括README.md都会改变该步骤的哈希值。解决严格限定COPY的范围。对于依赖安装只复制go.mod和go.sum。对于构建只复制src/、cmd/等源代码目录。6.2 构建速度没有想象中快未使用远程缓存本地缓存只对你自己有用。团队协作和 CI 环境中必须配置远程缓存如 S3才能实现缓存共享。这是提升 CI 构建速度最有效的一步。Target 粒度过粗如果你把下载依赖、编译代码、运行测试都放在一个 target 里那么任何代码改动都会导致“下载依赖”这个本应高度稳定的步骤缓存失效。务必拆分开。网络问题Earthly 构建时每个FROM和RUN如果涉及下载都会在容器内进行。确保你的基础镜像源和下载地址如 npm registry, pip index是高速可用的。可以在基础 Earthfile 中预先配置镜像源。6.3 如何调试 Earthly 构建earthly --verbose target输出极其详细的日志包括每个步骤的计算哈希、缓存查询结果等。这是排查缓存问题的利器。earthly --no-cache target强制忽略所有缓存全新构建。用于验证构建的确定性和正确性。earthly --save-inline-cache --push target在推送镜像的同时将缓存也推送到远程。这对于在 CI 中为后续构建准备缓存非常有用。交互式调试Earthly 目前不直接支持docker run -it那样的交互式进入容器。如果某个RUN步骤失败你需要仔细查看错误日志或者尝试将该步骤的命令拆分并在本地模拟容器环境进行测试。6.4 与现有 Docker 工具链的兼容性docker build参数Earthly 不完全支持所有docker build的参数。例如--build-arg在 Earthly 中是通过ARG指令在Earthfile内部声明的或者在命令行通过earthly --build-arg keyvalue传递。Docker ComposeEarthly 的WITH DOCKER可以启动 Compose 服务但如果你现有的开发流程严重依赖docker-compose up进行本地开发Earthly 并不会取代它。Earthly 主要负责“构建”和“测试”阶段的自动化本地开发时的服务编排可以继续使用 Docker Compose。两者是互补关系。6.5 成本考量Earthly Satellites作为商业功能需要订阅付费。你需要评估它带来的开发效率提升和本地资源节省是否值得这笔开销。对于小型团队或开源项目本地构建远程缓存可能已足够。远程缓存存储使用 S3 等云存储作为远程缓存后端会产生存储和流量费用。但通常这部分成本极低因为缓存是增量更新的且构建日志等中间产物不会存入缓存。经过几个项目的深度使用Earthly 已经彻底改变了我对构建系统的看法。它带来的最大价值不是某个单点速度的提升而是一种工程秩序的建立。构建逻辑变得清晰、可复用、可测试缓存行为变得可预测本地与 CI 环境达到高度一致。这些特性共同作用显著降低了与构建相关的维护成本和认知负担。如果你正在为混乱的 Dockerfile、缓慢的 CI 构建或是脆弱的构建流程而头疼那么投入时间学习和引入 Earthly很可能是一笔非常划算的技术投资。