
最近折腾一个多模块 Spring Boot 项目的 Docker 镜像构建,踩了不少坑,这篇把问题说清楚。
场景是这样的:你只改了一行 application.yaml 配置。
然后重新 build 镜像。
然后 Docker 开始下载半个互联网的依赖。
我之前做的一个多模块 Spring Boot 项目,有多个 pom.xml 和巨大的依赖树。每次 rebuild 都让人崩溃,有时候光解析 Maven 依赖就要 8-10 分钟,还没开始打包应用呢。
最坑的是什么?
实际代码改动就那么一点点。
是 Docker 层策略。
很多 Dockerfile 是这样写的:
COPY . .
RUN mvn package
看起来很简单。
但这完全破坏了 Docker 的层缓存机制。
每个 Docker 指令都会创建一个独立的不可变层。
每个层都有自己的内容哈希。如果任何一个层变了,Docker 会让下面所有层全部失效并重建。
所以如果 Dockerfile 在解析依赖之前先复制了源码:
COPY src ./src
RUN mvn package
那每次源码改动都会强制触发:
全部重来一遍。
尽管依赖压根没变过。
这就是构建时间浪费的大头。
我换上了 Docker BuildKit。
在 Dockerfile 顶部加这一行:
# syntax=docker/dockerfile:1.4
这会让 Dockerfile 前端连接到现代的 BuildKit 后端,解锁像缓存挂载这类高级特性。
然后不用普通的依赖解析:
RUN mvn dependency:go-offline
我改成了:
RUN --mount=type=cache,target=/root/.m2 \
mvn dependency:go-offline
这就是那个 game changer。
--mount=type=cache 到底干了什么Maven 的依赖通常存在:
/root/.m2
没有 BuildKit 的情况下:
用 BuildKit 缓存挂载的话:
所以第一次构建之后:
我的重建时间从好几分钟直接掉到几秒钟。
Dockerfile 里的指令顺序非常关键。
这个模式极其重要:
COPY pom.xml .
RUN mvn dependency:go-offline COPY src ./src
为什么?
因为 pom.xml 变化的频率远低于应用源码。
Docker 现在可以把依赖解析和源码改动分开缓存了。
所以:
如果你把顺序反过来:
COPY src ./src
COPY pom.xml .
那每次源码改动都会让后面所有层失效。
对大型 Java 构建来说这是灾难性的。
这时候我自己又冒出一个问题。
如果 GitHub Actions 这类 CI runner 每次运行都用全新的临时机器,那缓存到底存在哪?
因为每次流水线跑完:
.m2 缓存也没了那跨构建的缓存是怎么保持的?
答案是 BuildKit 远程缓存导出/导入。
在 GitHub Actions 里可以这样把 BuildKit 缓存持久化到跨 runner:
steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 - uses: docker/build-push-action@v5 with: context: . file: ./Dockerfile tags: myimage:latest cache-from: type=gha cache-to: type=gha,mode=max
这玩意儿强得离谱。
这里发生了什么:
cache-to 把 BuildKit 缓存推到 GitHub 的临时缓存存储cache-from 在后续工作流运行中恢复它所以尽管每个 runner 都是全新的:
这就是现代生产级 CI/CD 系统在大规模下优化容器构建的方式。
这不只是关乎开发者体验。
在生产工程环境中这直接影响:
对于部署了几十甚至上百个服务的团队来说,哪怕每次构建节省 5 分钟,都会累积成巨大的工程效率提升。
特别是在这些场景:
如果你的 Docker 构建慢得要命,别急着甩锅给:
大多数情况下真正的问题是 Dockerfile 层设计糟糕 + 缺少缓存策略。
一个结构合理的 Dockerfile 配合 BuildKit 缓存,能把重建时间从 10 分钟砍到几秒钟。
一旦体验过那种速度差异,就再也回不去了。