跳转至

Earthly:Dockerfile + Makefile 混合 CI 构建

为什么要学 Earthly

CI/CD 构建系统有两个经典痛点:

  1. "在 CI 上跑失败了,但本地没问题" —— 因为本地和 CI 环境不同
  2. Makefile 不可移植,Dockerfile 不支持多步骤编排 —— 两者各有所长但不能互补

Earthly 的核心洞察是:将 Dockerfile 的容器化隔离和 Makefile 的多目标编排合二为一。每个构建步骤都在容器中执行(保证可复现),同时支持依赖声明、缓存、并行执行和跨项目引用。

你写的 Earthfile 无论在本地还是 CI 上,行为完全一致。

核心优势: - 本地和 CI 行为一致(容器化执行) - 语法融合 Dockerfile 和 Makefile 的优点 - 内置缓存(比 Docker layer cache 更智能) - 原生并行执行 - Monorepo 友好 - 不绑定任何 CI 平台


核心概念

白话解释

概念白话说明
Earthfile构建配置文件(类似 Dockerfile + Makefile 的混合体)
Target一个构建目标(类似 Makefile 的 target),用 + 前缀表示
Artifact构建产物(文件),可以在 target 之间传递
ImageDocker 镜像输出
FROM基础镜像(和 Dockerfile 一样)
COPY复制文件(扩展了 Dockerfile 语法,可以从其他 target 复制)
RUN执行命令
SAVE ARTIFACT声明构建产物
SAVE IMAGE声明输出镜像
BUILD调用其他 target(类似 make 的依赖)

Earthly vs 其他构建系统

特性EarthlyMakefileDockerfileGitHub ActionsBazel
容器化隔离部分沙箱
本地/CI一致部分
学习曲线
缓存智能手动层级手动精确
并行自动有限声明式自动
Monorepo
语言无关

安装配置

安装

# macOS
brew install earthly

# Linux
sudo /bin/sh -c 'wget https://github.com/earthly/earthly/releases/latest/download/earthly-linux-amd64 -O /usr/local/bin/earthly && chmod +x /usr/local/bin/earthly'

# Windows (WSL)
sudo /bin/sh -c 'wget https://github.com/earthly/earthly/releases/latest/download/earthly-linux-amd64 -O /usr/local/bin/earthly && chmod +x /usr/local/bin/earthly'

初始化

# 启动 earthly daemon(需要 Docker)
earthly bootstrap

# 验证
earthly --version

前置要求

  • Docker(Earthly 使用 BuildKit 后端)
  • Linux / macOS / Windows (WSL)

快速上手

第一个 Earthfile

项目结构:

my-project/
├── Earthfile
├── src/
│   └── main.py
├── requirements.txt
└── tests/
    └── test_main.py

Earthfile

VERSION 0.8

# 基础目标:安装依赖
deps:
    FROM python:3.12-slim
    WORKDIR /app
    COPY requirements.txt .
    RUN pip install -r requirements.txt

# 构建目标
build:
    FROM +deps
    COPY src/ ./src/
    SAVE ARTIFACT src /src

# 测试目标
test:
    FROM +deps
    COPY src/ ./src/
    COPY tests/ ./tests/
    RUN pytest tests/ -v

# 生成 Docker 镜像
docker:
    FROM +deps
    COPY src/ ./src/
    ENTRYPOINT ["python", "src/main.py"]
    SAVE IMAGE my-app:latest

# 一键执行所有
all:
    BUILD +test
    BUILD +docker

执行

# 运行测试
earthly +test

# 构建镜像
earthly +docker

# 执行全部
earthly +all

# 查看可用 targets
earthly ls

输出产物到本地

# 将构建的镜像加载到本地 Docker
earthly +docker

# 将文件产物输出到本地
earthly +build --artifact src output/

进阶用法

1. 多语言 Monorepo

monorepo/
├── Earthfile          ← 根 Earthfile
├── services/
│   ├── api/
│   │   ├── Earthfile  ← 子项目 Earthfile
│   │   └── ...
│   └── worker/
│       ├── Earthfile
│       └── ...
└── libs/
    └── shared/
        ├── Earthfile
        └── ...

Earthfile

VERSION 0.8

all:
    BUILD ./services/api+docker
    BUILD ./services/worker+docker

test:
    BUILD ./services/api+test
    BUILD ./services/worker+test
    BUILD ./libs/shared+test

子项目 services/api/Earthfile

VERSION 0.8

deps:
    FROM python:3.12-slim
    WORKDIR /app
    # 引用共享库的产物
    COPY ../libs/shared+build/lib ./lib/
    COPY requirements.txt .
    RUN pip install -r requirements.txt

test:
    FROM +deps
    COPY . .
    RUN pytest

docker:
    FROM +deps
    COPY . .
    SAVE IMAGE api:latest

2. 并行构建

VERSION 0.8

# 这三个 BUILD 会自动并行执行(无依赖关系)
all:
    BUILD +lint
    BUILD +test
    BUILD +build

lint:
    FROM +deps
    RUN ruff check .

test:
    FROM +deps
    COPY tests/ ./tests/
    RUN pytest

build:
    FROM +deps
    COPY src/ ./src/
    RUN python -m build
    SAVE ARTIFACT dist/* AS LOCAL ./dist/

3. 参数化构建

VERSION 0.8

ARG --global PYTHON_VERSION=3.12
ARG --global REGISTRY=ghcr.io/myorg

deps:
    FROM python:${PYTHON_VERSION}-slim
    WORKDIR /app
    COPY requirements.txt .
    RUN pip install -r requirements.txt

docker:
    ARG tag=latest
    FROM +deps
    COPY src/ ./src/
    ENTRYPOINT ["python", "src/main.py"]
    SAVE IMAGE ${REGISTRY}/my-app:${tag}

使用:

# 使用默认参数
earthly +docker

# 覆盖参数
earthly +docker --tag=v1.2.3 --PYTHON_VERSION=3.11

4. 缓存优化

VERSION 0.8

deps:
    FROM node:20-slim
    WORKDIR /app
    # 先复制 package.json(缓存依赖安装步骤)
    COPY package.json package-lock.json .
    RUN --mount=type=cache,target=/root/.npm \
        npm ci
    # 再复制源码
    COPY . .

test:
    FROM +deps
    # 使用持久化缓存加速测试
    RUN --mount=type=cache,target=./node_modules/.cache \
        npm test

build:
    FROM +deps
    RUN npm run build
    SAVE ARTIFACT dist /dist

5. 多平台构建

VERSION 0.8

docker:
    # 同时构建 amd64  arm64
    BUILD --platform=linux/amd64 --platform=linux/arm64 +image

image:
    FROM python:3.12-slim
    COPY src/ /app/
    ENTRYPOINT ["python", "/app/main.py"]
    SAVE IMAGE my-app:latest

6. Secret 管理

VERSION 0.8

deploy:
    FROM +build
    # Secret 不会进入缓存或镜像层
    RUN --secret AWS_ACCESS_KEY_ID \
        --secret AWS_SECRET_ACCESS_KEY \
        aws s3 cp dist/ s3://my-bucket/ --recursive

使用:

# 从环境变量传递
earthly --secret AWS_ACCESS_KEY_ID --secret AWS_SECRET_ACCESS_KEY +deploy

# 从文件传递
earthly --secret-file github_token=~/.secrets/github_token +publish

7. CI 集成

GitHub Actions:

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: earthly/actions-setup@v1
      - name: Run tests
        run: earthly +test
      - name: Build and push
        run: earthly --push +docker
        env:
          EARTHLY_TOKEN: ${{ secrets.EARTHLY_TOKEN }}

GitLab CI:

# .gitlab-ci.yml
build:
  image: earthly/earthly:latest
  services:
    - docker:dind
  script:
    - earthly --ci +all

8. 集成测试(WITH DOCKER)

VERSION 0.8

integration-test:
    FROM earthly/dind:alpine-3.19
    WORKDIR /app
    COPY docker-compose.test.yml .
    COPY +build/app ./app

    # 在构建步骤中启动 Docker 容器
    WITH DOCKER \
        --compose docker-compose.test.yml \
        --load app:latest=+docker
        RUN docker compose up -d && \
            sleep 5 && \
            ./app/run-integration-tests.sh
    END

9. 远程构建(Earthly Satellites)

# 使用远程构建节点(更快的 CI)
earthly --org myorg --satellite my-satellite +build

# 远程缓存共享(团队成员共享构建缓存)
earthly --remote-cache=ghcr.io/myorg/cache +build

常见问题

Q1: 和 Dockerfile 有什么区别

维度EarthfileDockerfile
多阶段命名 target,可复用有限的 multi-stage
产物导出SAVE ARTIFACT 到本地只能导出镜像
调用其他文件BUILD ./sub/+target不支持
并行自动并行
参数ARG + 命令行ARG(有限)
测试可以作为 target不适合

Q2: Earthly 的 cache 为什么比 Docker 好

  • Docker 的 layer cache 是按行顺序的,前面一层变了后面全部失效
  • Earthly 使用 BuildKit 的高级缓存:mount cache、跨 target 缓存、远程缓存共享

Q3: 需要 Docker 才能用吗

是的。Earthly 底层使用 BuildKit(Docker 的构建引擎),需要 Docker 守护进程。但你不需要写 Dockerfile,Earthfile 替代了它。

Q4: 构建很慢

# 1. 检查缓存是否生效
earthly --verbose +target

# 2. 使用 mount cache 加速依赖安装
# RUN --mount=type=cache,target=/root/.cache/pip pip install ...

# 3. 分离依赖安装和代码复制
# 4. 使用远程缓存(团队共享)
earthly --remote-cache=registry/cache +build

Q5: 如何调试构建失败

# 交互式调试(进入失败步骤的容器)
earthly --interactive +target

# 显示详细日志
earthly --verbose +target

# 不使用缓存(排除缓存问题)
earthly --no-cache +target

参考资源

资源链接
官方网站https://earthly.dev
GitHub 仓库https://github.com/earthly/earthly
文档https://docs.earthly.dev
Earthfile 参考https://docs.earthly.dev/docs/earthfile
示例项目https://github.com/earthly/earthly/tree/main/examples
VS Code 扩展marketplace 搜索 "Earthfile Syntax Highlighting"
博客https://earthly.dev/blog

小结: Earthly 用一个优雅的抽象——"容器化的 Makefile"——解决了 CI 构建中最顽固的问题:可复现性。你写的 Earthfile 在任何机器上行为一致,不再有"CI 上挂了但本地跑不出来"的噩梦。如果你厌倦了维护复杂的 CI YAML 和不可靠的构建缓存,Earthly 值得尝试。